diff options
244 files changed, 30804 insertions, 11705 deletions
diff --git a/.ci/format.sh b/.ci/format.sh index d3b629c3..e1e6c1e4 100755 --- a/.ci/format.sh +++ b/.ci/format.sh @@ -11,5 +11,7 @@ FILES=$(find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \)) for f in $FILES do - clang-format -i "$f" && git diff --exit-code + clang-format -i "$f" done; + +git diff --exit-code diff --git a/.ci/install.sh b/.ci/install.sh index d8dd67f2..d1bff54b 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -2,14 +2,13 @@ set -ex +if [ "$FLATPAK" ]; then + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --noninteractive install --user flathub org.kde.Platform//5.14 + flatpak --noninteractive install --user flathub org.kde.Sdk//5.14 + exit +fi if [ "$TRAVIS_OS_NAME" = "osx" ]; then - brew update - brew install qt5 lmdb clang-format ninja libsodium cmark - brew upgrade boost cmake icu4c || true - - brew tap nlohmann/json - brew install --with-cmake nlohmann_json - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py sudo python get-pip.py @@ -21,34 +20,12 @@ fi if [ "$TRAVIS_OS_NAME" = "linux" ]; then + sudo update-alternatives --install /usr/bin/gcc gcc "/usr/bin/${CC}" 10 + sudo update-alternatives --install /usr/bin/g++ g++ "/usr/bin/${CXX}" 10 + + sudo update-alternatives --set gcc "/usr/bin/${CC}" + sudo update-alternatives --set g++ "/usr/bin/${CXX}" - if [ -z "$QT_VERSION" ]; then - QT_VERSION="592" - QT_PKG="59" - fi - - wget https://cmake.org/files/v3.12/cmake-3.12.2-Linux-x86_64.sh - sudo sh cmake-3.12.2-Linux-x86_64.sh --skip-license --prefix=/usr/local - - mkdir -p build-libsodium - ( cd build-libsodium - curl -L https://download.libsodium.org/libsodium/releases/libsodium-1.0.16.tar.gz -o libsodium-1.0.16.tar.gz - tar xfz libsodium-1.0.16.tar.gz - cd libsodium-1.0.16/ - ./configure && make && make check && sudo make install ) - - sudo add-apt-repository -y ppa:beineri/opt-qt${QT_VERSION}-trusty - # needed for git-lfs, otherwise the follow apt update fails. - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157 - - # needed for mongodb repository: https://github.com/travis-ci/travis-ci/issues/9037 - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6 - - sudo apt update -qq - sudo apt install -qq -y \ - qt${QT_PKG}base \ - qt${QT_PKG}tools \ - qt${QT_PKG}svg \ - qt${QT_PKG}multimedia \ - liblmdb-dev + wget https://cmake.org/files/v3.15/cmake-3.15.5-Linux-x86_64.sh + sudo sh cmake-3.15.5-Linux-x86_64.sh --skip-license --prefix=/usr/local fi diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 403fde14..ff8ef6ca 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -25,8 +25,8 @@ for iconSize in 16 32 48 64 128 256 512; do done # Only download the file when not already present -if ! [ -f linuxdeployqt-continuous-x86_64.AppImage ] ; then - wget -c "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" +if ! [ -f linuxdeployqt-6-x86_64.AppImage ] ; then + wget -c "https://github.com/probonopd/linuxdeployqt/releases/download/6/linuxdeployqt-6-x86_64.AppImage" fi chmod a+x linuxdeployqt*.AppImage @@ -36,7 +36,7 @@ unset LD_LIBRARY_PATH ARCH=$(uname -m) export ARCH -LD_LIBRARY_PATH=$(pwd)/.deps/usr/lib/:$LD_LIBRARY_PATH +LD_LIBRARY_PATH=$(pwd)/.deps/usr/lib/:/usr/local/lib/:$LD_LIBRARY_PATH export LD_LIBRARY_PATH for res in ./linuxdeployqt*.AppImage @@ -44,12 +44,14 @@ do linuxdeployqt=$res done -./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs -./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -appimage +./"$linuxdeployqt" ${DIR}/usr/share/applications/*.desktop -unsupported-allow-new-glibc -bundle-non-qt-libs -qmldir=./resources/qml -appimage chmod +x nheko-*x86_64.AppImage -if [ ! -z "$VERSION" ]; then +mkdir artifacts +cp nheko-*x86_64.AppImage artifacts/ + +if [ -n "$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" diff --git a/.ci/macos/deploy.sh b/.ci/macos/deploy.sh index 45ed13bc..1dc9472d 100755 --- a/.ci/macos/deploy.sh +++ b/.ci/macos/deploy.sh @@ -16,7 +16,7 @@ PATH=/usr/local/opt/qt/bin/:${PATH} 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 + sudo macdeployqt nheko.app -dmg -always-overwrite -qmldir=../resources/qml/ user=$(id -nu) sudo chown "${user}" nheko.dmg @@ -25,6 +25,8 @@ PATH=/usr/local/opt/qt/bin/:${PATH} dmgbuild -s ./.ci/macos/settings.json "Nheko" nheko.dmg -if [ ! -z "$VERSION" ]; then +if [ -n "$VERSION" ]; then mv nheko.dmg "nheko-${VERSION}.dmg" + mkdir artifacts + cp "nheko-${VERSION}.dmg" artifacts/ fi diff --git a/.ci/script.sh b/.ci/script.sh index cf8b524b..53bb1d9e 100755 --- a/.ci/script.sh +++ b/.ci/script.sh @@ -2,17 +2,25 @@ set -ex +if [ "$FLATPAK" ]; then + mkdir -p build-flatpak + cd build-flatpak + + flatpak-builder --ccache --repo=repo --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.json + flatpak build-bundle repo nheko-${VERSION}-${ARCH}.flatpak io.github.NhekoReborn.Nheko 0.7.0-dev + + mkdir ../artifacts + mv nheko-*.flatpak ../artifacts + + exit +fi + if [ "$TRAVIS_OS_NAME" = "linux" ]; then - export CC=${C_COMPILER} - export CXX=${CXX_COMPILER} # make build use all available cores export CMAKE_BUILD_PARALLEL_LEVEL=$(cat /proc/cpuinfo | awk '/^processor/{print $3}' | wc -l) - sudo update-alternatives --install /usr/bin/gcc gcc "/usr/bin/${C_COMPILER}" 10 - sudo update-alternatives --install /usr/bin/g++ g++ "/usr/bin/${CXX_COMPILER}" 10 - - sudo update-alternatives --set gcc "/usr/bin/${C_COMPILER}" - sudo update-alternatives --set g++ "/usr/bin/${CXX_COMPILER}" + export PATH="/usr/local/bin/:${PATH}" + cmake --version fi if [ "$TRAVIS_OS_NAME" = "linux" ]; then @@ -24,27 +32,39 @@ if [ "$TRAVIS_OS_NAME" = "osx" ]; then export CMAKE_PREFIX_PATH=/usr/local/opt/qt5 fi -# Build & install dependencies -cmake -GNinja -Hdeps -B.deps \ - -DUSE_BUNDLED_BOOST="${USE_BUNDLED_BOOST}" \ - -DUSE_BUNDLED_CMARK="${USE_BUNDLED_CMARK}" \ - -DUSE_BUNDLED_JSON="${USE_BUNDLED_JSON}" -cmake --build .deps +mkdir -p .deps/usr .hunter # Build nheko + +if [ "$TRAVIS_OS_NAME" = "osx" ]; then cmake -GNinja -H. -Bbuild \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_INSTALL_PREFIX=.deps/usr + -DCMAKE_INSTALL_PREFIX=.deps/usr \ + -DHUNTER_ROOT=".hunter" \ + -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \ + -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5 \ + -DCI_BUILD=ON +else +cmake -GNinja -H. -Bbuild \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_INSTALL_PREFIX=.deps/usr \ + -DHUNTER_ROOT=".hunter" \ + -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \ + -DUSE_BUNDLED_OPENSSL=OFF \ + -DCI_BUILD=ON +fi cmake --build build if [ "$TRAVIS_OS_NAME" = "osx" ]; then make lint; - if [ "$DEPLOYMENT" = 1 ] && [ ! -z "$VERSION" ] ; then + if [ "$DEPLOYMENT" = 1 ] && [ -n "$VERSION" ] ; then make macos-deploy; fi fi -if [ "$TRAVIS_OS_NAME" = "linux" ] && [ "$DEPLOYMENT" = 1 ] && [ ! -z "$VERSION" ]; then +if [ "$TRAVIS_OS_NAME" = "linux" ] && [ "$DEPLOYMENT" = 1 ] && [ -n "$VERSION" ]; then make linux-deploy; fi diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 706b00cc..fcda66fd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -20,9 +20,9 @@ If you're planning to work on a new feature leave a message on the Matrix room Example for a Japanese translation. - Create a new translation file using the prototype in English - - e.g `cp resources/langs/nheko_en.ts resources/langs/nheko_jp.ts` + - e.g `cp resources/langs/nheko_en.ts resources/langs/nheko_ja.ts` - Open the new translation file and change the line regarding the locale to reflect the current language. - - e.g `<TS version="2.1" language="en">` => `<TS version="2.1" language="jp">` + - e.g `<TS version="2.1" language="en">` => `<TS version="2.1" language="ja">` - Run `make update-translations` to update the translation files with any missing text. - Fill out the translation file (Qt Linguist can make things easier). - Submit a PR! diff --git a/.gitignore b/.gitignore index e9c854d0..6d178679 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,16 @@ -build +/build* tags +cscope* .clang_complete *wintoastlib* +/.ccls-cache +/.exrc +.gdb_history + +# GTAGS +GTAGS +GRTAGS +GPATH # C++ objects and libs @@ -31,6 +40,7 @@ moc_*.cpp qrc_*.cpp ui_*.h *-build-* +/.clangd/ # QtCreator @@ -43,6 +53,10 @@ ui_*.h *.qmlproject.user *.qmlproject.user.* +# Vim +*.swp +*.swo + #####=== CMake ===##### CMakeCache.txt diff --git a/.travis.yml b/.travis.yml index 952e56c2..ac3512bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,73 +1,138 @@ language: cpp sudo: required -dist: trusty +dist: xenial notifications: webhooks: urls: - - https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MHJlZF9za3klM0FvY2Vhbi5qb2Vkb25vZnJ5LmNvbS8lMjFldkFxa1BIWnVQSElHZWVuaGklM0FvY2Vhbi5qb2Vkb25vZnJ5LmNvbQ + - https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MHJlZF9za3klM0FuaGVrby5pbS8lMjFVYkNtSWxHVEhOSWdJUlpjcHQlM0FuaGVrby5pbQ on_success: always on_failure: always on_start: never email: false +cache: + directories: + - .hunter + - build-flatpak/.flatpak-builder + matrix: include: - os: osx compiler: clang - osx_image: xcode9 + # C++17 support + osx_image: xcode10.2 env: - - DEPLOYMENT=1 - - USE_BUNDLED_BOOST=0 - - USE_BUNDLED_CMARK=0 - - USE_BUNDLED_JSON=0 + - DEPLOYMENT=1 + addons: + homebrew: + taps: nlohmann/json + packages: + - clang-format + - cmake + - ninja + - openssl + - qt5 + update: true # workaround for broken travis homebrew - os: linux - compiler: gcc + compiler: gcc-7 env: - - CXX_COMPILER=g++-5 - - C_COMPILER=gcc-5 - - QT_VERSION="-5.10.1" - - QT_PKG=510 + - CXX=g++-7 + - CC=gcc-7 + - QT_PKG=512 - DEPLOYMENT=1 - - USE_BUNDLED_BOOST=1 - - USE_BUNDLED_CMARK=1 - - USE_BUNDLED_JSON=1 addons: apt: - sources: ["ubuntu-toolchain-r-test"] - packages: ["g++-5", "ninja-build"] + sources: + - ubuntu-toolchain-r-test + - sourceline: 'ppa:beineri/opt-qt-5.12.6-xenial' + packages: + - g++-7 + - ninja-build + - qt512base + - qt512tools + - qt512svg + - qt512multimedia + - qt512quickcontrols2 + - qt512graphicaleffects + - liblmdb-dev + - libgl1-mesa-dev # needed for missing gl.h - os: linux - compiler: gcc + compiler: gcc-8 env: - - CXX_COMPILER=g++-8 - - C_COMPILER=gcc-8 - - QT_VERSION=571 - - QT_PKG=57 - - USE_BUNDLED_BOOST=1 - - USE_BUNDLED_CMARK=1 - - USE_BUNDLED_JSON=1 + - CXX=g++-8 + - CC=gcc-8 + - QT_PKG=59 addons: apt: - sources: ["ubuntu-toolchain-r-test"] - packages: ["g++-8", "ninja-build"] + sources: + - ubuntu-toolchain-r-test + - sourceline: 'ppa:beineri/opt-qt597-xenial' + packages: + - g++-8 + - ninja-build + - qt59base + - qt59tools + - qt59svg + - qt59multimedia + - qt59quickcontrols2 + - qt59graphicaleffects + - liblmdb-dev + - libgl1-mesa-dev # needed for missing gl.h - os: linux - compiler: clang + compiler: clang-6 env: - - CXX_COMPILER=clang++-5.0 - - C_COMPILER=clang-5.0 - - QT_VERSION=592 + - CXX=clang++-6.0 + - CC=clang-6.0 - QT_PKG=59 - - USE_BUNDLED_BOOST=1 - - USE_BUNDLED_CMARK=1 - - USE_BUNDLED_JSON=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-xenial-6.0 + - sourceline: 'ppa:beineri/opt-qt597-xenial' + packages: + - clang++-6.0 + - g++-7 + - ninja-build + - qt59base + - qt59tools + - qt59svg + - qt59multimedia + - qt59quickcontrols2 + - qt59graphicaleffects + - liblmdb-dev + - libgl1-mesa-dev # needed for missing gl.h + - os: linux + env: + - DEPLOYMENT=1 + - FLATPAK=1 + - ARCH=amd64 + addons: + apt: + sources: + - sourceline: 'ppa:alexlarsson/flatpak' + packages: + - flatpak + - flatpak-builder + - elfutils + - os: linux + arch: arm64 + env: + - DEPLOYMENT=1 + - FLATPAK=1 + - ARCH=arm64 + addons: + apt: + sources: + - sourceline: 'ppa:alexlarsson/flatpak' + packages: + - flatpak + - flatpak-builder + - elfutils + - librsvg2-bin before_install: - - 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: @@ -79,6 +144,21 @@ script: - sed -i -e "s/VERSION_NAME_VALUE/${VERSION}/g" ./.ci/bintray-release.json || true - cp ./.ci/bintray-release.json . deploy: +- provider: s3 + access_key_id: $ARTIFACTS_KEY + secret_access_key: $ARTIFACTS_SECRET + bucket: $ARTIFACTS_BUCKET + region: $AWS_DEFAULT_REGION + detect_encoding: true + cache_control: "max-age=31536000" + skip_cleanup: true + acl: public_read + local_dir: artifacts + on: + condition: "$DEPLOYMENT == 1" + repo: Nheko-Reborn/nheko + tags: false + all_branches: true - provider: bintray user: "redsky17" key: diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f6c62f..55e59332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## [Unreleased] +### [0.7.0] -- Unreleased + +0.7.0 *requires* mtxclient 0.3.0. Make sure you compile against 0.3.0 +if you do not use the mtxclient bundled with nheko. + +#### Features +- Make nheko session import / export format match riot. Fixes #48 (WIP) +- Implement proper replies (WIP) +- Add .well-known support for auto-completing homeserver information +- Add mentions viewer so you can see all the messages you have been mentioned in (WIP) +- Add emoji font selection preference + +#### Improvements +- Add dedicated reply button to Timeline items. Add button for other options so + that right click isn't always required. +- Fix various things with regards to emoji rendering and the emoji picker (WIP) +- Lots and lots and lots of localization updates. +- Additional tweaks to the system theme + ## [0.6.4] - 2019-05-22 *Most* of the below fixes are due to updates in mtxclient. Make sure you compile against 0.2.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index fda60b71..146f2bb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,22 +1,86 @@ -cmake_minimum_required(VERSION 3.11) +cmake_minimum_required(VERSION 3.13) option(APPVEYOR_BUILD "Build on appveyor" OFF) +option(CI_BUILD "Set when building in CI. Enables -Werror where possible" OFF) option(ASAN "Compile with address sanitizers" OFF) - -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) - -add_definitions(-DBOOST_MPL_LIMIT_LIST_SIZE=30) -add_definitions(-DBOOST_MPL_CFG_NO_PREPROCESSED_HEADERS) +option(QML_DEBUGGING "Enable qml debugging" OFF) + +set( + CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/toolchain.cmake" + CACHE + FILEPATH "Default toolchain" + ) + + +option(HUNTER_ENABLED "Enable Hunter package manager" OFF) +include("cmake/HunterGate.cmake") +HunterGate( + URL "https://github.com/cpp-pm/hunter/archive/v0.23.244.tar.gz" + SHA1 "2c0f491fd0b80f7b09e3d21adb97237161ef9835" + LOCAL + ) + +option(USE_BUNDLED_BOOST "Use the bundled version of Boost." ${HUNTER_ENABLED}) +option(USE_BUNDLED_SPDLOG "Use the bundled version of spdlog." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_OLM "Use the bundled version of libolm." ${HUNTER_ENABLED}) +option(USE_BUNDLED_GTEST "Use the bundled version of Google Test." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_CMARK "Use the bundled version of cmark." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED}) +option(USE_BUNDLED_SODIUM "Use the bundled version of libsodium." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_ZLIB "Use the bundled version of zlib." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_LMDB "Use the bundled version of lmdb." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++." + ${HUNTER_ENABLED}) +option(USE_BUNDLED_TWEENY "Use the bundled version of tweeny." + ${HUNTER_ENABLED}) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") + +if(${CMAKE_VERSION} VERSION_LESS "3.14.0") + message("Adding FetchContent_MakeAvailable") + # from cmakes sources + macro(FetchContent_MakeAvailable) + + foreach(contentName IN ITEMS ${ARGV}) + string(TOLOWER ${contentName} contentNameLower) + FetchContent_GetProperties(${contentName}) + if(NOT ${contentNameLower}_POPULATED) + FetchContent_Populate(${contentName}) + + # Only try to call add_subdirectory() if the populated content + # can be treated that way. Protecting the call with the check + # allows this function to be used for projects that just want + # to ensure the content exists, such as to provide content at + # a known location. + if(EXISTS ${${contentNameLower}_SOURCE_DIR}/CMakeLists.txt) + add_subdirectory(${${contentNameLower}_SOURCE_DIR} + ${${contentNameLower}_BINARY_DIR}) + endif() + endif() + endforeach() + + endmacro() +endif() include(GNUInstallDirs) # Include Qt basic functions include(QtCommon) -project(nheko LANGUAGES C CXX) +project(nheko LANGUAGES CXX C) set(CPACK_PACKAGE_VERSION_MAJOR "0") -set(CPACK_PACKAGE_VERSION_MINOR "6") -set(CPACK_PACKAGE_VERSION_PATCH "4") +set(CPACK_PACKAGE_VERSION_MINOR "7") +set(CPACK_PACKAGE_VERSION_PATCH "0") set(PROJECT_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR}) set(PROJECT_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR}) set(PROJECT_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH}) @@ -27,132 +91,125 @@ fix_project_version() # Set additional project information set(COMPANY "Nheko") -set(COPYRIGHT "Copyright (c) 2018 Nheko Contributors") +set(COPYRIGHT "Copyright (c) 2019 Nheko Contributors") set(IDENTIFIER "com.github.mujx.nheko") add_project_meta(META_FILES_TO_INCLUDE) -if(APPLE) - set(OPENSSL_ROOT_DIR /usr/local/opt/openssl) -endif() - if(NOT MSVC AND NOT APPLE) - set(THREADS_PREFER_PTHREAD_FLAG ON) - find_package(Threads REQUIRED) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) endif() if (BUILD_DOCS) - find_package(Doxygen) - - if (DOXYGEN_FOUND) - set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Doxyfile.in) - set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) - - configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT}) - - add_custom_target(docs ALL - COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Generating API documentation with Doxygen" - VERBATIM ) - else (DOXYGEN_FOUND) - message("Doxygen need to be installed to generate the doxygen documentation") - endif (DOXYGEN_FOUND) + find_package(Doxygen) + + if (DOXYGEN_FOUND) + set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Doxyfile.in) + set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) + + configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT}) + + add_custom_target(docs ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM ) + else (DOXYGEN_FOUND) + message("Doxygen need to be installed to generate the doxygen documentation") + endif (DOXYGEN_FOUND) endif() # # LMDB # -include(LMDB) +#include(LMDB) +if(USE_BUNDLED_LMDB) + hunter_add_package(lmdb) + find_package(liblmdb CONFIG REQUIRED) +else() + find_package(LMDB) +endif() # # Discover Qt dependencies. # -find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia REQUIRED) +find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) +find_package(Qt5QuickCompiler) find_package(Qt5DBus) if (APPLE) - find_package(Qt5MacExtras REQUIRED) + find_package(Qt5MacExtras REQUIRED) endif(APPLE) if (Qt5Widgets_FOUND) - if (Qt5Widgets_VERSION VERSION_LESS 5.7.0) - message(STATUS "Qt version ${Qt5Widgets_VERSION}") - message(WARNING "Minimum supported Qt5 version is 5.7!") - endif() + if (Qt5Widgets_VERSION VERSION_LESS 5.9.0) + message(STATUS "Qt version ${Qt5Widgets_VERSION}") + message(WARNING "Minimum supported Qt5 version is 5.9!") + endif() endif(Qt5Widgets_FOUND) -# -# Set up compiler flags. -# -if (NOT MSVC) - set(CMAKE_C_COMPILER gcc) -endif(NOT MSVC) - -set(CMAKE_CXX_STANDARD 14) -set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) if(NOT MSVC) - set( - CMAKE_CXX_FLAGS - "${CMAKE_CXX_FLAGS} \ - -Wall \ - -Wextra \ - -Werror \ - -pipe \ - -pedantic \ - -fsized-deallocation \ - -fdiagnostics-color=always \ - -Wunreachable-code \ - -std=c++14" - ) - if (NOT CMAKE_COMPILER_IS_GNUCXX) - # -Wshadow is buggy and broken in GCC, so do not enable it. - # see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=79328 - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow") - endif() + set( + CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} \ + -Wall \ + -Wextra \ + -pipe \ + -pedantic \ + -fsized-deallocation \ + -fdiagnostics-color=always \ + -Wunreachable-code \ + -std=c++17" + ) + if (NOT CMAKE_COMPILER_IS_GNUCXX) + # -Wshadow is buggy and broken in GCC, so do not enable it. + # see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=79328 + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow") + endif() endif() if (MSVC) - set( - CMAKE_CXX_FLAGS - "${CMAKE_CXX_FLAGS} /bigobj" - ) + set( + CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} /bigobj" + ) endif() if(NOT (CMAKE_BUILD_TYPE OR CMAKE_CONFIGURATION_TYPES)) - set(CMAKE_BUILD_TYPE "Debug" CACHE STRING - "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel." - FORCE) - message("Setting build type to '${CMAKE_BUILD_TYPE}'") + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING + "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel." + FORCE) + message("Setting build type to '${CMAKE_BUILD_TYPE}'") else(NOT (CMAKE_BUILD_TYPE OR CMAKE_CONFIGURATION_TYPES)) - message("Build type set to '${CMAKE_BUILD_TYPE}'") + message("Build type set to '${CMAKE_BUILD_TYPE}'") endif(NOT (CMAKE_BUILD_TYPE OR CMAKE_CONFIGURATION_TYPES)) set(SPDLOG_DEBUG_ON false) # Windows doesn't handle CMAKE_BUILD_TYPE. if(NOT WIN32) - if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") - set(SPDLOG_DEBUG_ON true) - else() - set(SPDLOG_DEBUG_ON false) - endif() + if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") + set(SPDLOG_DEBUG_ON true) + else() + set(SPDLOG_DEBUG_ON false) + endif() endif() find_program(GIT git) if(GIT) - execute_process( - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMAND ${GIT} rev-parse --short HEAD - OUTPUT_VARIABLE GIT_OUT OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(GIT_OUT) - set(CPACK_PACKAGE_VERSION_PATCH "${CPACK_PACKAGE_VERSION_PATCH}-${GIT_OUT}") - else() - set(CPACK_PACKAGE_VERSION_PATCH "${CPACK_PACKAGE_VERSION_PATCH}") - endif() + execute_process( + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${GIT} rev-parse --short HEAD + OUTPUT_VARIABLE GIT_OUT OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(GIT_OUT) + set(CPACK_PACKAGE_VERSION_PATCH "${CPACK_PACKAGE_VERSION_PATCH}-${GIT_OUT}") + else() + set(CPACK_PACKAGE_VERSION_PATCH "${CPACK_PACKAGE_VERSION_PATCH}") + endif() endif(GIT) set(CPACK_PACKAGE_VERSION ${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}) @@ -169,221 +226,281 @@ configure_file(cmake/nheko.h config/nheko.h) # Declare source and header files. # set(SRC_FILES - # Dialogs - src/dialogs/CreateRoom.cpp - src/dialogs/ImageOverlay.cpp - src/dialogs/PreviewUploadOverlay.cpp - src/dialogs/InviteUsers.cpp - src/dialogs/JoinRoom.cpp - src/dialogs/MemberList.cpp - src/dialogs/LeaveRoom.cpp - src/dialogs/Logout.cpp - src/dialogs/UserProfile.cpp - src/dialogs/ReadReceipts.cpp - src/dialogs/ReCaptcha.cpp - 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 - src/timeline/TimelineViewManager.cpp - src/timeline/TimelineItem.cpp - src/timeline/TimelineView.cpp - src/timeline/widgets/AudioItem.cpp - src/timeline/widgets/FileItem.cpp - src/timeline/widgets/ImageItem.cpp - src/timeline/widgets/VideoItem.cpp - - # UI components - src/ui/Avatar.cpp - src/ui/Badge.cpp - src/ui/LoadingIndicator.cpp - src/ui/InfoMessage.cpp - src/ui/FlatButton.cpp - src/ui/FloatingButton.cpp - src/ui/Label.cpp - src/ui/OverlayModal.cpp - src/ui/SnackBar.cpp - src/ui/RaisedButton.cpp - src/ui/Ripple.cpp - src/ui/RippleOverlay.cpp - src/ui/OverlayWidget.cpp - src/ui/TextField.cpp - src/ui/TextLabel.cpp - src/ui/ToggleButton.cpp - src/ui/Theme.cpp - src/ui/ThemeManager.cpp - - src/AvatarProvider.cpp - src/Cache.cpp - src/ChatPage.cpp - src/CommunitiesListItem.cpp - src/CommunitiesList.cpp - src/InviteeItem.cpp - src/LoginPage.cpp - src/Logging.cpp - src/MainWindow.cpp - src/MatrixClient.cpp - src/QuickSwitcher.cpp - src/Olm.cpp - src/RegisterPage.cpp - src/RoomInfoListItem.cpp - src/RoomList.cpp - src/RunGuard.cpp - src/SideBarActions.cpp - src/Splitter.cpp - src/SuggestionsPopup.cpp - src/TextInputWidget.cpp - src/TopRoomBar.cpp - src/TrayIcon.cpp - src/TypingDisplay.cpp - src/Utils.cpp - src/UserInfoWidget.cpp - src/UserSettingsPage.cpp - src/WelcomePage.cpp - src/main.cpp -) - -# ExternalProject dependencies -set(EXTERNAL_PROJECT_DEPS "") + # Dialogs + src/dialogs/CreateRoom.cpp + src/dialogs/FallbackAuth.cpp + src/dialogs/ImageOverlay.cpp + src/dialogs/InviteUsers.cpp + src/dialogs/JoinRoom.cpp + src/dialogs/LeaveRoom.cpp + src/dialogs/Logout.cpp + src/dialogs/MemberList.cpp + src/dialogs/PreviewUploadOverlay.cpp + src/dialogs/ReCaptcha.cpp + src/dialogs/ReadReceipts.cpp + src/dialogs/RoomSettings.cpp + src/dialogs/UserProfile.cpp + + # Emoji + src/emoji/Category.cpp + src/emoji/ItemDelegate.cpp + src/emoji/Panel.cpp + src/emoji/PickButton.cpp + src/emoji/Provider.cpp + + # Timeline + src/timeline/TimelineViewManager.cpp + src/timeline/TimelineModel.cpp + src/timeline/DelegateChooser.cpp + + # UI components + src/ui/Avatar.cpp + src/ui/Badge.cpp + src/ui/DropShadow.cpp + src/ui/LoadingIndicator.cpp + src/ui/InfoMessage.cpp + src/ui/FlatButton.cpp + src/ui/FloatingButton.cpp + src/ui/Label.cpp + src/ui/OverlayModal.cpp + src/ui/SnackBar.cpp + src/ui/RaisedButton.cpp + src/ui/Ripple.cpp + src/ui/RippleOverlay.cpp + src/ui/OverlayWidget.cpp + src/ui/TextField.cpp + src/ui/TextLabel.cpp + src/ui/ToggleButton.cpp + src/ui/Theme.cpp + src/ui/ThemeManager.cpp + + src/AvatarProvider.cpp + src/Cache.cpp + src/ChatPage.cpp + src/CommunitiesListItem.cpp + src/CommunitiesList.cpp + src/EventAccessors.cpp + src/InviteeItem.cpp + src/LoginPage.cpp + src/Logging.cpp + src/MainWindow.cpp + src/MatrixClient.cpp + src/MxcImageProvider.cpp + src/ColorImageProvider.cpp + src/QuickSwitcher.cpp + src/Olm.cpp + src/RegisterPage.cpp + src/RoomInfoListItem.cpp + src/RoomList.cpp + src/SideBarActions.cpp + src/Splitter.cpp + src/popups/SuggestionsPopup.cpp + src/popups/PopupItem.cpp + src/popups/ReplyPopup.cpp + src/popups/UserMentions.cpp + src/TextInputWidget.cpp + src/TopRoomBar.cpp + src/TrayIcon.cpp + src/Utils.cpp + src/UserInfoWidget.cpp + src/UserSettingsPage.cpp + src/WelcomePage.cpp + src/main.cpp + ) + include(FeatureSummary) -set(Boost_USE_STATIC_LIBS OFF) -set(Boost_USE_STATIC_RUNTIME OFF) -set(Boost_USE_MULTITHREADED ON) -find_package(Boost 1.66 REQUIRED - COMPONENTS atomic - chrono - date_time - iostreams - random - regex - system - thread) +if(USE_BUNDLED_BOOST) + hunter_add_package(Boost COMPONENTS iostreams system thread) +endif() +find_package(Boost 1.70 REQUIRED + COMPONENTS iostreams + system + thread) +if(USE_BUNDLED_ZLIB) + hunter_add_package(ZLIB) +endif() find_package(ZLIB REQUIRED) +if(USE_BUNDLED_OPENSSL) + hunter_add_package(OpenSSL) +endif() find_package(OpenSSL REQUIRED) -find_package(MatrixClient 0.1.0 REQUIRED) -find_package(Olm 2 REQUIRED) +if(USE_BUNDLED_MTXCLIENT) + include(FetchContent) + set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") + set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") + FetchContent_Declare( + MatrixClient + GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git + GIT_TAG 7fc1d357afaabb134cb6d9c593f94915973d31fa + ) + FetchContent_MakeAvailable(MatrixClient) +else() + find_package(MatrixClient 0.3.0 REQUIRED) +endif() +if(USE_BUNDLED_OLM) + include(FetchContent) + set(OLM_TESTS OFF CACHE INTERNAL "") + FetchContent_Declare( + Olm + GIT_REPOSITORY https://gitlab.matrix.org/matrix-org/olm.git + GIT_TAG 3.1.4 + ) + FetchContent_MakeAvailable(Olm) +else() + find_package(Olm 3) + set_package_properties(Olm PROPERTIES + DESCRIPTION "An implementation of the Double Ratchet cryptographic ratchet" + URL "https://git.matrix.org/git/olm/about/" + TYPE REQUIRED + ) +endif() +if(USE_BUNDLED_SPDLOG) + hunter_add_package(spdlog) +endif() find_package(spdlog 1.0.0 CONFIG REQUIRED) -find_package(cmark REQUIRED) + +if(USE_BUNDLED_CMARK) + include(FetchContent) + FetchContent_Declare( + cmark + GIT_REPOSITORY https://github.com/commonmark/cmark.git + GIT_TAG 242e277a661ec7e51f34dcaf86c1925d550b1498 #0.29.0 << doesn't work with fetch content yet + CMAKE_ARGS "CMARK_STATIC=ON CMARK_SHARED=OFF CMARK_TESTS=OFF CMARK_TESTS=OFF" + ) + FetchContent_MakeAvailable(cmark) + if (MSVC) + add_library(cmark::cmark ALIAS libcmark) + else() + add_library(cmark::cmark ALIAS libcmark_static) + endif() +else() + find_package(cmark REQUIRED) +endif() + +if(USE_BUNDLED_JSON) + hunter_add_package(nlohmann_json) +endif() find_package(nlohmann_json 3.2.0) set_package_properties(nlohmann_json PROPERTIES - DESCRIPTION "JSON for Modern C++, a C++11 header-only JSON class" - URL "https://nlohmann.github.io/json/" - TYPE REQUIRED -) - -if(NOT LMDBXX_INCLUDE_DIR) - find_path(LMDBXX_INCLUDE_DIR - NAMES lmdb++.h - PATHS /usr/include - /usr/local/include - $ENV{LIB_DIR}/include - $ENV{LIB_DIR}/include/lmdbxx) -endif() -include_directories(SYSTEM ${LMDBXX_INCLUDE_DIR}) - -if(NOT TWEENY_INCLUDE_DIR) - find_path(TWEENY_INCLUDE_DIR - NAMES tweeny/tweeny.h - PATHS /usr/include/ - /usr/local/include/ - $ENV{LIB_DIR}/include/ - $ENV{LIB_DIR}/include/tweeny) + DESCRIPTION "JSON for Modern C++, a C++11 header-only JSON class" + URL "https://nlohmann.github.io/json/" + TYPE REQUIRED + ) + +if(USE_BUNDLED_LMDBXX) + hunter_add_package(lmdbxx) + find_package(lmdbxx CONFIG REQUIRED) +else() + if(NOT LMDBXX_INCLUDE_DIR) + find_path(LMDBXX_INCLUDE_DIR + NAMES lmdb++.h + PATHS /usr/include + /usr/local/include + $ENV{LIB_DIR}/include + $ENV{LIB_DIR}/include/lmdbxx) + + endif() + add_library(lmdbxx INTERFACE) + target_include_directories(lmdbxx INTERFACE ${LMDBXX_INCLUDE_DIR}) + add_library(lmdbxx::lmdbxx ALIAS lmdbxx) endif() -include_directories(SYSTEM ${TWEENY_INCLUDE_DIR}) -include_directories(${CMAKE_SOURCE_DIR}/src) -include_directories(${Boost_INCLUDE_DIRS}) +if(USE_BUNDLED_TWEENY) + include(FetchContent) + FetchContent_Declare( + Tweeny + GIT_REPOSITORY https://github.com/mobius3/tweeny.git + GIT_TAG 6a5033372fe53c4c731c66c8a2d56261746cd85c #v3 <- v3 has unfixed warnings + ) + FetchContent_MakeAvailable(Tweeny) +else() + find_package(Tweeny REQUIRED) +endif() -# local inclue directory -include_directories(includes) +# single instance functionality +set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") +add_subdirectory(third_party/SingleApplication-3.0.19/) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) qt5_wrap_cpp(MOC_HEADERS - # Dialogs - src/dialogs/CreateRoom.h - src/dialogs/ImageOverlay.h - src/dialogs/PreviewUploadOverlay.h - src/dialogs/InviteUsers.h - src/dialogs/JoinRoom.h - src/dialogs/MemberList.h - src/dialogs/LeaveRoom.h - src/dialogs/Logout.h - src/dialogs/UserProfile.h - src/dialogs/RawMessage.h - src/dialogs/ReadReceipts.h - 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 - src/timeline/TimelineViewManager.h - src/timeline/widgets/AudioItem.h - src/timeline/widgets/FileItem.h - src/timeline/widgets/ImageItem.h - src/timeline/widgets/VideoItem.h - - # UI components - src/ui/Avatar.h - src/ui/Badge.h - src/ui/LoadingIndicator.h - src/ui/InfoMessage.h - src/ui/FlatButton.h - src/ui/Label.h - src/ui/FloatingButton.h - src/ui/Menu.h - src/ui/OverlayWidget.h - src/ui/SnackBar.h - src/ui/RaisedButton.h - src/ui/Ripple.h - src/ui/RippleOverlay.h - src/ui/TextField.h - src/ui/TextLabel.h - src/ui/ToggleButton.h - src/ui/Theme.h - src/ui/ThemeManager.h - - src/notifications/Manager.h - - src/AvatarProvider.h - src/Cache.h - src/ChatPage.h - src/CommunitiesListItem.h - src/CommunitiesList.h - src/LoginPage.h - src/MainWindow.h - src/MatrixClient.h - src/InviteeItem.h - src/QuickSwitcher.h - src/RegisterPage.h - src/RoomInfoListItem.h - src/RoomList.h - src/SideBarActions.h - src/Splitter.h - src/SuggestionsPopup.h - src/TextInputWidget.h - src/TopRoomBar.h - src/TrayIcon.h - src/TypingDisplay.h - src/UserInfoWidget.h - src/UserSettingsPage.h - src/WelcomePage.h -) + # Dialogs + src/dialogs/CreateRoom.h + src/dialogs/FallbackAuth.h + src/dialogs/ImageOverlay.h + src/dialogs/InviteUsers.h + src/dialogs/JoinRoom.h + src/dialogs/LeaveRoom.h + src/dialogs/Logout.h + src/dialogs/MemberList.h + src/dialogs/PreviewUploadOverlay.h + src/dialogs/RawMessage.h + src/dialogs/ReCaptcha.h + src/dialogs/ReadReceipts.h + src/dialogs/RoomSettings.h + src/dialogs/UserProfile.h + + # Emoji + src/emoji/Category.h + src/emoji/ItemDelegate.h + src/emoji/Panel.h + src/emoji/PickButton.h + + # Timeline + src/timeline/TimelineViewManager.h + src/timeline/TimelineModel.h + src/timeline/DelegateChooser.h + + # UI components + src/ui/Avatar.h + src/ui/Badge.h + src/ui/LoadingIndicator.h + src/ui/InfoMessage.h + src/ui/FlatButton.h + src/ui/Label.h + src/ui/FloatingButton.h + src/ui/Menu.h + src/ui/OverlayWidget.h + src/ui/SnackBar.h + src/ui/RaisedButton.h + src/ui/Ripple.h + src/ui/RippleOverlay.h + src/ui/TextField.h + src/ui/TextLabel.h + src/ui/ToggleButton.h + src/ui/Theme.h + src/ui/ThemeManager.h + + src/notifications/Manager.h + + src/AvatarProvider.h + src/Cache_p.h + src/ChatPage.h + src/CommunitiesListItem.h + src/CommunitiesList.h + src/LoginPage.h + src/MainWindow.h + src/MxcImageProvider.h + src/InviteeItem.h + src/QuickSwitcher.h + src/RegisterPage.h + src/RoomInfoListItem.h + src/RoomList.h + src/SideBarActions.h + src/Splitter.h + src/popups/SuggestionsPopup.h + src/popups/ReplyPopup.h + src/popups/PopupItem.h + src/popups/UserMentions.h + src/TextInputWidget.h + src/TopRoomBar.h + src/TrayIcon.h + src/UserInfoWidget.h + src/UserSettingsPage.h + src/WelcomePage.h + ) # # Bundle translations. @@ -391,85 +508,104 @@ qt5_wrap_cpp(MOC_HEADERS include(Translations) set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC}) -set(COMMON_LIBS - MatrixClient::MatrixClient - ${Boost_LIBRARIES} - cmark::cmark - Qt5::Widgets - Qt5::Svg - Qt5::Concurrent - Qt5::Multimedia - nlohmann_json::nlohmann_json) - -if(APPVEYOR_BUILD) - set(NHEKO_LIBS ${COMMON_LIBS} lmdb) -else() - set(NHEKO_LIBS ${COMMON_LIBS} ${LMDB_LIBRARY}) -endif() - if (APPLE) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa") - set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa") + set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm) elseif (WIN32) - file(DOWNLOAD - "https://raw.githubusercontent.com/mohabouje/WinToast/41ed1c58d5dce0ee9c01dbdeac05be45358d4f57/src/wintoastlib.cpp" - ${PROJECT_SOURCE_DIR}/src/wintoastlib.cpp - EXPECTED_HASH SHA256=1A1A7CE41C1052B12946798F4A6C67CE1FAD209C967F5ED4D720B173527E2073) + file(DOWNLOAD + "https://raw.githubusercontent.com/mohabouje/WinToast/41ed1c58d5dce0ee9c01dbdeac05be45358d4f57/src/wintoastlib.cpp" + ${PROJECT_SOURCE_DIR}/src/wintoastlib.cpp + EXPECTED_HASH SHA256=1A1A7CE41C1052B12946798F4A6C67CE1FAD209C967F5ED4D720B173527E2073) - file(DOWNLOAD - "https://raw.githubusercontent.com/mohabouje/WinToast/41ed1c58d5dce0ee9c01dbdeac05be45358d4f57/src/wintoastlib.h" - ${PROJECT_SOURCE_DIR}/src/wintoastlib.h - EXPECTED_HASH SHA256=b4481023c5782733795838be22bf1a75f45d87458cd4d9a5a75f664a146eea11) + file(DOWNLOAD + "https://raw.githubusercontent.com/mohabouje/WinToast/41ed1c58d5dce0ee9c01dbdeac05be45358d4f57/src/wintoastlib.h" + ${PROJECT_SOURCE_DIR}/src/wintoastlib.h + EXPECTED_HASH SHA256=b4481023c5782733795838be22bf1a75f45d87458cd4d9a5a75f664a146eea11) - set(SRC_FILES ${SRC_FILES} src/notifications/ManagerWin.cpp src/wintoastlib.cpp) + set(SRC_FILES ${SRC_FILES} src/notifications/ManagerWin.cpp src/wintoastlib.cpp) else () - set(SRC_FILES ${SRC_FILES} src/notifications/ManagerLinux.cpp) + set(SRC_FILES ${SRC_FILES} src/notifications/ManagerLinux.cpp) endif () set(NHEKO_DEPS - ${SRC_FILES} - ${UI_HEADERS} - ${MOC_HEADERS} - ${TRANSLATION_DEPS} - ${META_FILES_TO_INCLUDE}) + ${SRC_FILES} + ${UI_HEADERS} + ${MOC_HEADERS} + ${TRANSLATION_DEPS} + ${META_FILES_TO_INCLUDE}) if(ASAN) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined") endif() +add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) if(APPLE) - add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) - target_link_libraries (nheko ${NHEKO_LIBS} Qt5::MacExtras) + target_link_libraries (nheko PRIVATE Qt5::MacExtras) elseif(WIN32) - add_executable (nheko ${OS_BUNDLE} ${ICON_FILE} ${NHEKO_DEPS}) - target_link_libraries (nheko ${NTDLIB} ${NHEKO_LIBS} Qt5::WinMain) + target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN) + target_link_libraries (nheko PRIVATE ${NTDLIB} Qt5::WinMain) else() - add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS}) - target_link_libraries (nheko ${NHEKO_LIBS} Qt5::DBus) + target_link_libraries (nheko PRIVATE Qt5::DBus) +endif() +target_include_directories(nheko PRIVATE src includes) + +target_link_libraries(nheko PRIVATE + MatrixClient::MatrixClient + Boost::iostreams + Boost::system + Boost::thread + cmark::cmark + spdlog::spdlog + Qt5::Widgets + Qt5::Svg + Qt5::Concurrent + Qt5::Multimedia + Qt5::Qml + Qt5::QuickControls2 + Qt5::QuickWidgets + nlohmann_json::nlohmann_json + lmdbxx::lmdbxx + liblmdb::lmdb + tweeny + SingleApplication::SingleApplication) + +if(MSVC) + target_link_libraries(nheko PRIVATE ntdll) endif() -if(EXTERNAL_PROJECT_DEPS) - add_dependencies(nheko ${EXTERNAL_PROJECT_DEPS}) + +if(QML_DEBUGGING) + target_compile_definitions(nheko PRIVATE QML_DEBUGGING) endif() + +if(NOT MSVC) + if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR CI_BUILD) + target_compile_options(nheko PRIVATE "-Werror") + endif() +endif() + +set_target_properties(nheko PROPERTIES SKIP_BUILD_RPATH TRUE) + if(UNIX AND NOT APPLE) - install (TARGETS nheko RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") - install (FILES "resources/nheko-16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "nheko.png") - install (FILES "resources/nheko-32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "nheko.png") - install (FILES "resources/nheko-48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "nheko.png") - install (FILES "resources/nheko-64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "nheko.png") - install (FILES "resources/nheko-128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "nheko.png") - install (FILES "resources/nheko-256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "nheko.png") - install (FILES "resources/nheko-512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "nheko.png") - install (FILES "resources/nheko.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") - install (FILES "resources/nheko.appdata.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") - - if(NOT TARGET uninstall) - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" - IMMEDIATE @ONLY) - add_custom_target(uninstall - COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) - endif() + install (TARGETS nheko RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") + install (FILES "resources/nheko-16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "nheko.png") + install (FILES "resources/nheko-32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "nheko.png") + install (FILES "resources/nheko-48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "nheko.png") + install (FILES "resources/nheko-64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "nheko.png") + install (FILES "resources/nheko-128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "nheko.png") + install (FILES "resources/nheko-256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "nheko.png") + install (FILES "resources/nheko-512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "nheko.png") + install (FILES "resources/nheko.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps" RENAME "nheko.svg") + install (FILES "resources/nheko.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") + install (FILES "resources/nheko.appdata.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") + + if(NOT TARGET uninstall) + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" + IMMEDIATE @ONLY) + add_custom_target(uninstall + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) + endif() endif() diff --git a/Dockerfile b/Dockerfile index 2e01b40b..dddd1c6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN \ add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ apt-get update -qq && \ apt-get install -y \ - qt510base qt510tools qt510svg qt510multimedia \ + qt510base qt510tools qt510svg qt510multimedia qt510quickcontrols2 qt510graphicaleffects \ gcc-5 g++-5 RUN \ @@ -44,4 +44,4 @@ ENV PATH=/opt/qt510/bin:$PATH RUN mkdir /build -WORKDIR /build \ No newline at end of file +WORKDIR /build diff --git a/Makefile b/Makefile index 2f688d3b..7f603dcb 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ update-translations: -locations relative \ -Iinclude/dialogs \ -Iinclude \ - src/ -ts resources/langs/nheko_*.ts -no-obsolete + src/ resources/qml/ -ts resources/langs/nheko_*.ts -no-obsolete clean: rm -rf build diff --git a/README.md b/README.md index ed0cb0fb..db522f70 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ nheko [![Build Status](https://travis-ci.org/Nheko-Reborn/nheko.svg?branch=master)](https://travis-ci.org/Nheko-Reborn/nheko) [![Build status](https://ci.appveyor.com/api/projects/status/07qrqbfylsg4hw2h/branch/master?svg=true)](https://ci.appveyor.com/project/redsky17/nheko/branch/master) [![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.6.4) -[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://bintray.com/nheko-reborn/nheko/nheko) +[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://nheko-reborn-artifacts.s3.us-east-2.amazonaws.com/list.html) [![#nheko-reborn:matrix.org](https://img.shields.io/matrix/nheko-reborn:matrix.org.svg?label=%23nheko-reborn:matrix.org)](https://matrix.to/#/#nheko-reborn:matrix.org) [![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko) <a href='https://flathub.org/apps/details/io.github.NhekoReborn.Nheko'><img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a> @@ -11,21 +11,28 @@ nheko The motivation behind the project is to provide a native desktop app for [Matrix] that feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IRC client. +### Translations ### +[![Translation status](http://weblate.nheko.im/widgets/nheko/-/nheko-master/svg-badge.svg)](http://weblate.nheko.im/engage/nheko/?utm_source=widget) + +Help us with translations so as many people as possible will be able to use nheko! + ### Note regarding End-to-End encryption Currently the implementation is at best a **proof of concept** and it should only be used for -testing purposes. +testing purposes. Most importantly, it is missing device verification, so while your messages +and media are encrypted, nheko doesn't verify who gets the messages. ## Features Most of the features you would expect from a chat application are missing right now but we are getting close to a more feature complete client. Specifically there is support for: -- E2E encryption (text messages only: attachments are currently sent unencrypted). +- E2E encryption. - User registration. - Creating, joining & leaving rooms. - Sending & receiving invites. - Sending & receiving files and emoji (inline widgets for images, audio and file messages). +- Replies with text, images and other media (and actually render them as inline widgets). - Typing notifications. - Username auto-completion. - Message & mention notifications. @@ -62,7 +69,7 @@ sudo dnf install nheko #### Gentoo Linux ```bash -sudo layman -a matrix +sudo eselect repository enable matrix sudo emerge -a nheko ``` @@ -86,7 +93,8 @@ flatpak install flathub io.github.NhekoReborn.Nheko guix install nheko ``` -#### macOS (10.12 and above) +#### macOS (10.14 and above) + with [macports](https://www.macports.org/) : @@ -96,21 +104,43 @@ sudo port install nheko ### Build Requirements -- Qt5 (5.7 or greater). Qt 5.7 adds support for color font rendering with - Freetype, which is essential to properly support emoji. -- CMake 3.1 or greater. +- Qt5 (5.10 or greater). Qt 5.7 adds support for color font rendering with + Freetype, which is essential to properly support emoji, 5.8 adds some features + to make interopability with Qml easier, 5.10 makes sliders actually visible with different palettes. +- CMake 3.15 or greater. (Lower version may work, but may break boost linking) - [mtxclient](https://github.com/Nheko-Reborn/mtxclient) - [LMDB](https://symas.com/lightning-memory-mapped-database/) -- [cmark](https://github.com/commonmark/cmark) -- Boost 1.66 or greater. +- [cmark](https://github.com/commonmark/cmark) 0.29 or greater. +- Boost 1.70 or greater. - [libolm](https://git.matrix.org/git/olm) - [libsodium](https://github.com/jedisct1/libsodium) - [spdlog](https://github.com/gabime/spdlog) -- A compiler that supports C++ 14: - - Clang 5 (tested on Travis CI) +- A compiler that supports C++ 17: + - Clang 6 (tested on Travis CI) - GCC 7 (tested on Travis CI) - MSVC 19.13 (tested on AppVeyor) +Nheko can use bundled version for most of those libraries automatically, if the versions in your distro are too old. +To use them, you can enable the hunter integration by passing `-DHUNTER_ENABLED=ON`. +It is probably wise to link those dependencies statically by passing `-DBUILD_SHARED_LIBS=OFF` +You can select which bundled dependencies you want to use py passing various `-DUSE_BUNDLED_*` flags. By default all dependencies are bundled *if* you enable hunter. + +The bundle flags are currently: + +- USE_BUNDLED_BOOST +- USE_BUNDLED_SPDLOG +- USE_BUNDLED_OLM +- USE_BUNDLED_GTEST +- USE_BUNDLED_CMARK +- USE_BUNDLED_JSON +- USE_BUNDLED_OPENSSL +- USE_BUNDLED_MTXCLIENT +- USE_BUNDLED_SODIUM +- USE_BUNDLED_ZLIB +- USE_BUNDLED_LMDB +- USE_BUNDLED_LMDBXX +- USE_BUNDLED_TWEENY + #### Linux If you don't want to install any external dependencies, you can generate an AppImage locally using docker. @@ -138,26 +168,45 @@ sudo pacman -S qt5-base \ ##### Gentoo Linux ```bash -sudo emerge -a ">=dev-qt/qtgui-5.7.1" media-libs/fontconfig +sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig ``` -##### Ubuntu (e.g 14.04) +##### Ubuntu 16.04 ```bash -sudo add-apt-repository ppa:beineri/opt-qt592-trusty +sudo add-apt-repository ppa:beineri/opt-qt592-xenial sudo add-apt-repository ppa:george-edison55/cmake-3.x sudo add-apt-repository ppa:ubuntu-toolchain-r-test sudo apt-get update sudo apt-get install -y g++-7 qt59base qt59svg qt59tools qt59multimedia cmake liblmdb-dev libsodium-dev ``` +##### Ubuntu 19.10 + +```bash +# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports): +sudo apt install g++-7 cmake liblmdb-dev libsodium-dev qt{base,tools,multimedia}5-dev qml-module-qt{gstreamer,multimedia,quick-extras} libqt5svg5-dev qt{script,quickcontrols2-}5-dev +``` + +##### Debian Buster (or higher probably) + +(User report, not sure if all of those are needed) + +```bash +sudo apt install cmake gcc make automake liblmdb-dev libsodium-dev \ + qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \ + qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools \ + qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts +``` + ##### Guix ```bash guix environment nheko ``` -##### macOS (Xcode 8 or later) +##### macOS (Xcode 10.2 or later) + ```bash brew update @@ -170,61 +219,29 @@ brew install qt5 lmdb cmake llvm libsodium spdlog boost cmark (for the CMake integration) workloads. 2. Download the latest Qt for windows installer and install it somewhere. -Make sure to install the `MSVC 2017 64-bit` toolset for at least Qt 5.9 +Make sure to install the `MSVC 2017 64-bit` toolset for at least Qt 5.10 (lower versions does not support VS2017). -3. Install dependencies with `vcpkg`. You can simply clone it into a subfolder -of the root nheko source directory. - -```powershell -git clone http:\\github.com\Microsoft\vcpkg -cd vcpkg -.\bootstrap-vcpkg.bat -.\vcpkg install --triplet x64-windows \ - boost-asio \ - boost-beast \ - boost-iostreams \ - boost-random \ - boost-signals2 \ - boost-system \ - boost-thread \ - cmark \ - libsodium \ - lmdb \ - openssl \ - zlib -``` - -4. Install dependencies not managed by vcpkg. (libolm, libmtxclient, libmatrix_structs) - -Inside the project root run the following (replacing the path to vcpkg as necessary). - -```bash -cmake -G "Visual Studio 15 2017 Win64" -Hdeps -B.deps - -DCMAKE_TOOLCHAIN_FILE=C:/Users/<your-path>/vcpkg/scripts/buildsystems/vcpkg.cmake - -DUSE_BUNDLED_BOOST=OFF -cmake --build .deps --config Release -cmake --build .deps --config Debug -``` +3. If you don't have openssl installed, you will need to install perl to build it (i.e. Strawberry Perl). ### Building -First we need to install the rest of the dependencies that are not available in our system +We can now build nheko: ```bash -cmake -Hdeps -B.deps \ - -DUSE_BUNDLED_BOOST=OFF # if we already have boost & spdlog installed. - -DUSE_BUNDLED_SPDLOG=OFF -cmake --build .deps +cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release +cmake --build build ``` -We can now build nheko by pointing it to the path that we installed the dependencies. +To use bundled dependencies you can use hunter, i.e.: ```bash -cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=.deps/usr -cmake --build build +cmake -H. -Bbuild -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=OFF +cmake --build build --config Release ``` +Adapt the USE_BUNDLED_* as needed. + If the build fails with the following error ``` Could not find a package configuration file provided by "Qt5Widgets" with @@ -249,13 +266,14 @@ The `nheko` binary will be located in the `build` directory. After installing all dependencies, you need to edit the `CMakeSettings.json` to be able to load and compile nheko within Visual Studio. -You need to fill out the paths for the `CMAKE_TOOLCHAIN_FILE` and the `Qt5_DIR`. -The toolchain file should point to the `vcpkg.cmake` and the Qt5 dir to the `lib\cmake\Qt5` dir. +You need to fill out the paths for the `Qt5_DIR`. +The Qt5 dir should point to the `lib\cmake\Qt5` dir. Examples for the paths are: - - `C:\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake` - `C:\\Qt\\5.10.1\\msvc2017_64\\lib\\cmake\\Qt5` +You should also enable hunter by setting `HUNTER_ENABLED` to `ON` and `BUILD_SHARED_LIBS` to `OFF`. + Now right click into the root nheko source directory and choose `Open in Visual Studio`. You can choose the build type Release and Debug in the top toolbar. After a successful CMake generation you can select the `nheko.exe` as the run target. @@ -273,6 +291,9 @@ windeployqt nheko.exe The final binary will be located inside `build-vc\Release\Release` for the Release build and `build-vc\Debug\Debug` for the Debug build. +Also copy the respective cmark.dll to the binary dir from `build/cmark-build/src/Release` (or Debug). + + ### Contributing See [CONTRIBUTING](.github/CONTRIBUTING.md) @@ -288,9 +309,7 @@ Here are some screen shots to get a feel for the UI, but things will probably ch ### Third party -- [Emoji One](http://emojione.com) -- [Font Awesome](http://fontawesome.io/) -- [Open Sans](https://fonts.google.com/specimen/Open+Sans) +[Single Application for Qt](https://github.com/itay-grudev/SingleApplication) [Matrix]:https://matrix.org [Riot]:https://riot.im diff --git a/appveyor.yml b/appveyor.yml index 08251174..78b57139 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ --- -version: 0.6.4-{build} +version: 0.7.0-{build} configuration: Release image: Visual Studio 2017 @@ -10,30 +10,17 @@ environment: BINTRAY_APIKEY: secure: "iGl5mzE9/ta9kFELUxDw9XtlYMSCMai9xowXIkYzU8WKHz7NfW0mLwMJZvblZFXJ" -cache: c:\tools\vcpkg\installed\ +cache: + - c:\hunter\ -> appveyor.yml + - build\_deps -> appveyor.yml,deps\CMakeLists.txt build: verbosity: minimal install: - - set QT_DIR=C:\Qt\5.10.1\msvc2017_64 - - set PATH=%PATH%;%QT_DIR%\bin;C:\MinGW\bin - - set PATH=%PATH%;C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\bin + - set QT_DIR=C:\Qt\5.13\msvc2017_64 + - set PATH=%PATH%;%QT_DIR%\bin - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" - - cd "C:\Tools\vcpkg"&& git pull && .\bootstrap-vcpkg.bat && cd %APPVEYOR_BUILD_FOLDER% - - vcpkg install - nlohmann-json:%PLATFORM%-windows - boost-asio:%PLATFORM%-windows - boost-beast:%PLATFORM%-windows - boost-iostreams:%PLATFORM%-windows - boost-random:%PLATFORM%-windows - boost-signals2:%PLATFORM%-windows - boost-system:%PLATFORM%-windows - boost-thread:%PLATFORM%-windows - libsodium:%PLATFORM%-windows - lmdb:%PLATFORM%-windows - openssl:%PLATFORM%-windows - zlib:%PLATFORM%-windows build_script: # VERSION format: branch-master/branch-1.2 @@ -54,23 +41,13 @@ build_script: - echo %INSTVERSION% - echo %DATE% - # Build & install the dependencies - - cmake -G "Visual Studio 15 2017 Win64" -Hdeps -B.deps - -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake - -DUSE_BUNDLED_BOOST=OFF - -DUSE_BUNDLED_JSON=OFF - -DMTX_STATIC=ON - - cmake --build .deps --config Release - # Build nheko - - rm -f cmake/FindOlm.cmake + #- cmake -G "Visual Studio 16 2019" -A x64 -H. -Bbuild - cmake -G "Visual Studio 15 2017 Win64" -H. -Bbuild - -DCMAKE_TOOLCHAIN_FILE=C:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake - -DLMDBXX_INCLUDE_DIR=.deps/usr/include - -DTWEENY_INCLUDE_DIR=.deps/usr/include - -DCMARK_INCLUDE_DIR=C:/projects/nheko/.deps/usr/include - -DCMARK_LIBRARY=C:/projects/nheko/.deps/usr/lib/cmark.lib - -DJSON_INCLUDE_DIR=.deps/usr/include + -DHUNTER_ROOT="C:\hunter" + -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF + -DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Release + - cmake --build build --config Release after_build: @@ -79,14 +56,9 @@ after_build: - echo %BUILD% - mkdir NhekoRelease - copy build\Release\nheko.exe NhekoRelease\nheko.exe + - copy build\_deps\cmark-build\src\Release\cmark.dll NhekoRelease\cmark.dll - windeployqt --qmldir %QT_DIR%\qml\ --release NhekoRelease\nheko.exe - - copy C:\Tools\vcpkg\installed\x64-windows\lib\*.lib .\NhekoRelease\ - - copy C:\Tools\vcpkg\installed\x64-windows\bin\*.dll .\NhekoRelease\ - - - copy C:\projects\nheko\.deps\usr\lib\cmark.lib .\NhekoRelease\ - - copy C:\projects\nheko\.deps\usr\bin\cmark.dll .\NhekoRelease\ - - 7z a nheko_win_64.zip .\NhekoRelease\* - ls -lh build\Release\ - ls -lh NhekoRelease\ @@ -135,15 +107,22 @@ on_success: - if "%APPVEYOR_REPO_TAG%" == "true" (curl -T nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe -uredsky17:%BINTRAY_APIKEY% https://api.bintray.com/content/nheko-reborn/nheko/%APPVEYOR_REPO_TAG_NAME%/nheko/%APPVEYOR_REPO_TAG_NAME%/) deploy: - description: "Development builds" - provider: GitHub - auth_token: - secure: "ShStWeqp+TkYqJPQr7uFZb+B8ZTgC7Iwth+IkhjfRDCTLhy8gtWvlPzlQilder3E" - artifact: nheko-${APPVEYOR_REPO_TAG_NAME}-installer.exe - force_update: true - prerelease: true - on: - appveyor_repo_tag: true + - description: "Development builds" + provider: GitHub + auth_token: + secure: "ShStWeqp+TkYqJPQr7uFZb+B8ZTgC7Iwth+IkhjfRDCTLhy8gtWvlPzlQilder3E" + artifact: nheko-${APPVEYOR_REPO_TAG_NAME}-installer.exe + force_update: true + prerelease: true + on: + appveyor_repo_tag: true + - provider: S3 + access_key_id: ${AWS_ACCESS_KEY} + secret_access_key: ${AWS_SECRET_KEY} + bucket: ${AWS_BUCKET_NAME} + region: ${AWS_DEFAULT_REGION} + set_public: true + artifact: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe, nheko_win_64.zip artifacts: - path: nheko_win_64.zip diff --git a/cmake/FindLMDB.cmake b/cmake/FindLMDB.cmake new file mode 100644 index 00000000..372dc7a5 --- /dev/null +++ b/cmake/FindLMDB.cmake @@ -0,0 +1,15 @@ +# +# Find the lmdb library & include dir. +# + +find_path (LMDB_INCLUDE_DIR NAMES lmdb.h PATHS "$ENV{LMDB_DIR}/include") +find_library (LMDB_LIBRARY NAMES lmdb PATHS "$ENV{LMDB_DIR}/lib" ) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LMDB DEFAULT_MSG LMDB_INCLUDE_DIR LMDB_LIBRARY) + + +add_library(lmdb INTERFACE IMPORTED GLOBAL) +target_include_directories(lmdb INTERFACE ${LMDB_INCLUDE_DIR}) +target_link_libraries(lmdb INTERFACE ${LMDB_LIBRARY}) + +add_library(liblmdb::lmdb ALIAS lmdb) diff --git a/cmake/FindOlm.cmake b/cmake/FindOlm.cmake deleted file mode 100644 index da68dbf5..00000000 --- a/cmake/FindOlm.cmake +++ /dev/null @@ -1,44 +0,0 @@ -# -# CMake module to search for the olm library -# -# On success, the macro sets the following variables: -# OLM_FOUND = if the library found -# OLM_LIBRARY = full path to the library -# OLM_INCLUDE_DIR = where to find the library headers -# -if(WIN32) - message(STATUS "FindOlm is not supported in Windows") - return() -endif() - -find_path(OLM_INCLUDE_DIR - NAMES olm/olm.h - PATHS /usr/include - /usr/local/include - $ENV{LIB_DIR}/include - $ENV{LIB_DIR}/include/olm) - -find_library(OLM_LIBRARY - NAMES olm - PATHS /usr/lib /usr/local/lib $ENV{LIB_DIR}/lib) - -if(OLM_FOUND) - set(OLM_INCLUDE_DIRS ${OLM_INCLUDE_DIR}) - - if(NOT OLM_LIBRARIES) - set(OLM_LIBRARIES ${OLM_LIBRARY}) - endif() -endif() - -if(NOT TARGET Olm::Olm) - add_library(Olm::Olm UNKNOWN IMPORTED) - set_target_properties(Olm::Olm - PROPERTIES INTERFACE_INCLUDE_DIRECTORIES - ${OLM_INCLUDE_DIR}) - set_property(TARGET Olm::Olm APPEND PROPERTY IMPORTED_LOCATION ${OLM_LIBRARY}) -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(OLM DEFAULT_MSG OLM_INCLUDE_DIR OLM_LIBRARY) - -mark_as_advanced(OLM_LIBRARY OLM_INCLUDE_DIR) diff --git a/cmake/Hunter/config.cmake b/cmake/Hunter/config.cmake new file mode 100644 index 00000000..d2f87774 --- /dev/null +++ b/cmake/Hunter/config.cmake @@ -0,0 +1,5 @@ +hunter_config( + Boost + VERSION "1.70.0-p0" + CMAKE_ARGS IOSTREAMS_NO_BZIP2=1 +) diff --git a/cmake/HunterGate.cmake b/cmake/HunterGate.cmake new file mode 100644 index 00000000..e78d3e89 --- /dev/null +++ b/cmake/HunterGate.cmake @@ -0,0 +1,528 @@ +# Copyright (c) 2013-2019, Ruslan Baratov +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This is a gate file to Hunter package manager. +# Include this file using `include` command and add package you need, example: +# +# cmake_minimum_required(VERSION 3.2) +# +# include("cmake/HunterGate.cmake") +# HunterGate( +# URL "https://github.com/path/to/hunter/archive.tar.gz" +# SHA1 "798501e983f14b28b10cda16afa4de69eee1da1d" +# ) +# +# project(MyProject) +# +# hunter_add_package(Foo) +# hunter_add_package(Boo COMPONENTS Bar Baz) +# +# Projects: +# * https://github.com/hunter-packages/gate/ +# * https://github.com/ruslo/hunter + +option(HUNTER_ENABLED "Enable Hunter package manager support" ON) + +if(HUNTER_ENABLED) + if(CMAKE_VERSION VERSION_LESS "3.2") + message( + FATAL_ERROR + "At least CMake version 3.2 required for Hunter dependency management." + " Update CMake or set HUNTER_ENABLED to OFF." + ) + endif() +endif() + +include(CMakeParseArguments) # cmake_parse_arguments + +option(HUNTER_STATUS_PRINT "Print working status" ON) +option(HUNTER_STATUS_DEBUG "Print a lot info" OFF) +option(HUNTER_TLS_VERIFY "Enable/disable TLS certificate checking on downloads" ON) + +set(HUNTER_ERROR_PAGE "https://docs.hunter.sh/en/latest/reference/errors") + +function(hunter_gate_status_print) + if(HUNTER_STATUS_PRINT OR HUNTER_STATUS_DEBUG) + foreach(print_message ${ARGV}) + message(STATUS "[hunter] ${print_message}") + endforeach() + endif() +endfunction() + +function(hunter_gate_status_debug) + if(HUNTER_STATUS_DEBUG) + foreach(print_message ${ARGV}) + string(TIMESTAMP timestamp) + message(STATUS "[hunter *** DEBUG *** ${timestamp}] ${print_message}") + endforeach() + endif() +endfunction() + +function(hunter_gate_error_page error_page) + message("------------------------------ ERROR ------------------------------") + message(" ${HUNTER_ERROR_PAGE}/${error_page}.html") + message("-------------------------------------------------------------------") + message("") + message(FATAL_ERROR "") +endfunction() + +function(hunter_gate_internal_error) + message("") + foreach(print_message ${ARGV}) + message("[hunter ** INTERNAL **] ${print_message}") + endforeach() + message("[hunter ** INTERNAL **] [Directory:${CMAKE_CURRENT_LIST_DIR}]") + message("") + hunter_gate_error_page("error.internal") +endfunction() + +function(hunter_gate_fatal_error) + cmake_parse_arguments(hunter "" "ERROR_PAGE" "" "${ARGV}") + if("${hunter_ERROR_PAGE}" STREQUAL "") + hunter_gate_internal_error("Expected ERROR_PAGE") + endif() + message("") + foreach(x ${hunter_UNPARSED_ARGUMENTS}) + message("[hunter ** FATAL ERROR **] ${x}") + endforeach() + message("[hunter ** FATAL ERROR **] [Directory:${CMAKE_CURRENT_LIST_DIR}]") + message("") + hunter_gate_error_page("${hunter_ERROR_PAGE}") +endfunction() + +function(hunter_gate_user_error) + hunter_gate_fatal_error(${ARGV} ERROR_PAGE "error.incorrect.input.data") +endfunction() + +function(hunter_gate_self root version sha1 result) + string(COMPARE EQUAL "${root}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("root is empty") + endif() + + string(COMPARE EQUAL "${version}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("version is empty") + endif() + + string(COMPARE EQUAL "${sha1}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("sha1 is empty") + endif() + + string(SUBSTRING "${sha1}" 0 7 archive_id) + + set( + hunter_self + "${root}/_Base/Download/Hunter/${version}/${archive_id}/Unpacked" + ) + + set("${result}" "${hunter_self}" PARENT_SCOPE) +endfunction() + +# Set HUNTER_GATE_ROOT cmake variable to suitable value. +function(hunter_gate_detect_root) + # Check CMake variable + string(COMPARE NOTEQUAL "${HUNTER_ROOT}" "" not_empty) + if(not_empty) + set(HUNTER_GATE_ROOT "${HUNTER_ROOT}" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT detected by cmake variable") + return() + endif() + + # Check environment variable + string(COMPARE NOTEQUAL "$ENV{HUNTER_ROOT}" "" not_empty) + if(not_empty) + set(HUNTER_GATE_ROOT "$ENV{HUNTER_ROOT}" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT detected by environment variable") + return() + endif() + + # Check HOME environment variable + string(COMPARE NOTEQUAL "$ENV{HOME}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{HOME}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT set using HOME environment variable") + return() + endif() + + # Check SYSTEMDRIVE and USERPROFILE environment variable (windows only) + if(WIN32) + string(COMPARE NOTEQUAL "$ENV{SYSTEMDRIVE}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{SYSTEMDRIVE}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug( + "HUNTER_ROOT set using SYSTEMDRIVE environment variable" + ) + return() + endif() + + string(COMPARE NOTEQUAL "$ENV{USERPROFILE}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{USERPROFILE}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug( + "HUNTER_ROOT set using USERPROFILE environment variable" + ) + return() + endif() + endif() + + hunter_gate_fatal_error( + "Can't detect HUNTER_ROOT" + ERROR_PAGE "error.detect.hunter.root" + ) +endfunction() + +function(hunter_gate_download dir) + string( + COMPARE + NOTEQUAL + "$ENV{HUNTER_DISABLE_AUTOINSTALL}" + "" + disable_autoinstall + ) + if(disable_autoinstall AND NOT HUNTER_RUN_INSTALL) + hunter_gate_fatal_error( + "Hunter not found in '${dir}'" + "Set HUNTER_RUN_INSTALL=ON to auto-install it from '${HUNTER_GATE_URL}'" + "Settings:" + " HUNTER_ROOT: ${HUNTER_GATE_ROOT}" + " HUNTER_SHA1: ${HUNTER_GATE_SHA1}" + ERROR_PAGE "error.run.install" + ) + endif() + string(COMPARE EQUAL "${dir}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("Empty 'dir' argument") + endif() + + string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("HUNTER_GATE_SHA1 empty") + endif() + + string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("HUNTER_GATE_URL empty") + endif() + + set(done_location "${dir}/DONE") + set(sha1_location "${dir}/SHA1") + + set(build_dir "${dir}/Build") + set(cmakelists "${dir}/CMakeLists.txt") + + hunter_gate_status_debug("Locking directory: ${dir}") + file(LOCK "${dir}" DIRECTORY GUARD FUNCTION) + hunter_gate_status_debug("Lock done") + + if(EXISTS "${done_location}") + # while waiting for lock other instance can do all the job + hunter_gate_status_debug("File '${done_location}' found, skip install") + return() + endif() + + file(REMOVE_RECURSE "${build_dir}") + file(REMOVE_RECURSE "${cmakelists}") + + file(MAKE_DIRECTORY "${build_dir}") # check directory permissions + + # Disabling languages speeds up a little bit, reduces noise in the output + # and avoids path too long windows error + file( + WRITE + "${cmakelists}" + "cmake_minimum_required(VERSION 3.2)\n" + "project(HunterDownload LANGUAGES NONE)\n" + "include(ExternalProject)\n" + "ExternalProject_Add(\n" + " Hunter\n" + " URL\n" + " \"${HUNTER_GATE_URL}\"\n" + " URL_HASH\n" + " SHA1=${HUNTER_GATE_SHA1}\n" + " DOWNLOAD_DIR\n" + " \"${dir}\"\n" + " TLS_VERIFY\n" + " ${HUNTER_TLS_VERIFY}\n" + " SOURCE_DIR\n" + " \"${dir}/Unpacked\"\n" + " CONFIGURE_COMMAND\n" + " \"\"\n" + " BUILD_COMMAND\n" + " \"\"\n" + " INSTALL_COMMAND\n" + " \"\"\n" + ")\n" + ) + + if(HUNTER_STATUS_DEBUG) + set(logging_params "") + else() + set(logging_params OUTPUT_QUIET) + endif() + + hunter_gate_status_debug("Run generate") + + # Need to add toolchain file too. + # Otherwise on Visual Studio + MDD this will fail with error: + # "Could not find an appropriate version of the Windows 10 SDK installed on this machine" + if(EXISTS "${CMAKE_TOOLCHAIN_FILE}") + get_filename_component(absolute_CMAKE_TOOLCHAIN_FILE "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE) + set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=${absolute_CMAKE_TOOLCHAIN_FILE}") + else() + # 'toolchain_arg' can't be empty + set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=") + endif() + + string(COMPARE EQUAL "${CMAKE_MAKE_PROGRAM}" "" no_make) + if(no_make) + set(make_arg "") + else() + # Test case: remove Ninja from PATH but set it via CMAKE_MAKE_PROGRAM + set(make_arg "-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}") + endif() + + execute_process( + COMMAND + "${CMAKE_COMMAND}" + "-H${dir}" + "-B${build_dir}" + "-G${CMAKE_GENERATOR}" + "${toolchain_arg}" + ${make_arg} + WORKING_DIRECTORY "${dir}" + RESULT_VARIABLE download_result + ${logging_params} + ) + + if(NOT download_result EQUAL 0) + hunter_gate_internal_error( + "Configure project failed." + "To reproduce the error run: ${CMAKE_COMMAND} -H${dir} -B${build_dir} -G${CMAKE_GENERATOR} ${toolchain_arg} ${make_arg}" + "In directory ${dir}" + ) + endif() + + hunter_gate_status_print( + "Initializing Hunter workspace (${HUNTER_GATE_SHA1})" + " ${HUNTER_GATE_URL}" + " -> ${dir}" + ) + execute_process( + COMMAND "${CMAKE_COMMAND}" --build "${build_dir}" + WORKING_DIRECTORY "${dir}" + RESULT_VARIABLE download_result + ${logging_params} + ) + + if(NOT download_result EQUAL 0) + hunter_gate_internal_error("Build project failed") + endif() + + file(REMOVE_RECURSE "${build_dir}") + file(REMOVE_RECURSE "${cmakelists}") + + file(WRITE "${sha1_location}" "${HUNTER_GATE_SHA1}") + file(WRITE "${done_location}" "DONE") + + hunter_gate_status_debug("Finished") +endfunction() + +# Must be a macro so master file 'cmake/Hunter' can +# apply all variables easily just by 'include' command +# (otherwise PARENT_SCOPE magic needed) +macro(HunterGate) + if(HUNTER_GATE_DONE) + # variable HUNTER_GATE_DONE set explicitly for external project + # (see `hunter_download`) + set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES) + endif() + + # First HunterGate command will init Hunter, others will be ignored + get_property(_hunter_gate_done GLOBAL PROPERTY HUNTER_GATE_DONE SET) + + if(NOT HUNTER_ENABLED) + # Empty function to avoid error "unknown function" + function(hunter_add_package) + endfunction() + + set( + _hunter_gate_disabled_mode_dir + "${CMAKE_CURRENT_LIST_DIR}/cmake/Hunter/disabled-mode" + ) + if(EXISTS "${_hunter_gate_disabled_mode_dir}") + hunter_gate_status_debug( + "Adding \"disabled-mode\" modules: ${_hunter_gate_disabled_mode_dir}" + ) + list(APPEND CMAKE_PREFIX_PATH "${_hunter_gate_disabled_mode_dir}") + endif() + elseif(_hunter_gate_done) + hunter_gate_status_debug("Secondary HunterGate (use old settings)") + hunter_gate_self( + "${HUNTER_CACHED_ROOT}" + "${HUNTER_VERSION}" + "${HUNTER_SHA1}" + _hunter_self + ) + include("${_hunter_self}/cmake/Hunter") + else() + set(HUNTER_GATE_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}") + + string(COMPARE NOTEQUAL "${PROJECT_NAME}" "" _have_project_name) + if(_have_project_name) + hunter_gate_fatal_error( + "Please set HunterGate *before* 'project' command. " + "Detected project: ${PROJECT_NAME}" + ERROR_PAGE "error.huntergate.before.project" + ) + endif() + + cmake_parse_arguments( + HUNTER_GATE "LOCAL" "URL;SHA1;GLOBAL;FILEPATH" "" ${ARGV} + ) + + string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" _empty_sha1) + string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" _empty_url) + string( + COMPARE + NOTEQUAL + "${HUNTER_GATE_UNPARSED_ARGUMENTS}" + "" + _have_unparsed + ) + string(COMPARE NOTEQUAL "${HUNTER_GATE_GLOBAL}" "" _have_global) + string(COMPARE NOTEQUAL "${HUNTER_GATE_FILEPATH}" "" _have_filepath) + + if(_have_unparsed) + hunter_gate_user_error( + "HunterGate unparsed arguments: ${HUNTER_GATE_UNPARSED_ARGUMENTS}" + ) + endif() + if(_empty_sha1) + hunter_gate_user_error("SHA1 suboption of HunterGate is mandatory") + endif() + if(_empty_url) + hunter_gate_user_error("URL suboption of HunterGate is mandatory") + endif() + if(_have_global) + if(HUNTER_GATE_LOCAL) + hunter_gate_user_error("Unexpected LOCAL (already has GLOBAL)") + endif() + if(_have_filepath) + hunter_gate_user_error("Unexpected FILEPATH (already has GLOBAL)") + endif() + endif() + if(HUNTER_GATE_LOCAL) + if(_have_global) + hunter_gate_user_error("Unexpected GLOBAL (already has LOCAL)") + endif() + if(_have_filepath) + hunter_gate_user_error("Unexpected FILEPATH (already has LOCAL)") + endif() + endif() + if(_have_filepath) + if(_have_global) + hunter_gate_user_error("Unexpected GLOBAL (already has FILEPATH)") + endif() + if(HUNTER_GATE_LOCAL) + hunter_gate_user_error("Unexpected LOCAL (already has FILEPATH)") + endif() + endif() + + hunter_gate_detect_root() # set HUNTER_GATE_ROOT + + # Beautify path, fix probable problems with windows path slashes + get_filename_component( + HUNTER_GATE_ROOT "${HUNTER_GATE_ROOT}" ABSOLUTE + ) + hunter_gate_status_debug("HUNTER_ROOT: ${HUNTER_GATE_ROOT}") + if(NOT HUNTER_ALLOW_SPACES_IN_PATH) + string(FIND "${HUNTER_GATE_ROOT}" " " _contain_spaces) + if(NOT _contain_spaces EQUAL -1) + hunter_gate_fatal_error( + "HUNTER_ROOT (${HUNTER_GATE_ROOT}) contains spaces." + "Set HUNTER_ALLOW_SPACES_IN_PATH=ON to skip this error" + "(Use at your own risk!)" + ERROR_PAGE "error.spaces.in.hunter.root" + ) + endif() + endif() + + string( + REGEX + MATCH + "[0-9]+\\.[0-9]+\\.[0-9]+[-_a-z0-9]*" + HUNTER_GATE_VERSION + "${HUNTER_GATE_URL}" + ) + string(COMPARE EQUAL "${HUNTER_GATE_VERSION}" "" _is_empty) + if(_is_empty) + set(HUNTER_GATE_VERSION "unknown") + endif() + + hunter_gate_self( + "${HUNTER_GATE_ROOT}" + "${HUNTER_GATE_VERSION}" + "${HUNTER_GATE_SHA1}" + _hunter_self + ) + + set(_master_location "${_hunter_self}/cmake/Hunter") + get_filename_component(_archive_id_location "${_hunter_self}/.." ABSOLUTE) + set(_done_location "${_archive_id_location}/DONE") + set(_sha1_location "${_archive_id_location}/SHA1") + + # Check Hunter already downloaded by HunterGate + if(NOT EXISTS "${_done_location}") + hunter_gate_download("${_archive_id_location}") + endif() + + if(NOT EXISTS "${_done_location}") + hunter_gate_internal_error("hunter_gate_download failed") + endif() + + if(NOT EXISTS "${_sha1_location}") + hunter_gate_internal_error("${_sha1_location} not found") + endif() + file(READ "${_sha1_location}" _sha1_value) + string(COMPARE EQUAL "${_sha1_value}" "${HUNTER_GATE_SHA1}" _is_equal) + if(NOT _is_equal) + hunter_gate_internal_error( + "Short SHA1 collision:" + " ${_sha1_value} (from ${_sha1_location})" + " ${HUNTER_GATE_SHA1} (HunterGate)" + ) + endif() + if(NOT EXISTS "${_master_location}") + hunter_gate_user_error( + "Master file not found:" + " ${_master_location}" + "try to update Hunter/HunterGate" + ) + endif() + include("${_master_location}") + set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES) + endif() +endmacro() diff --git a/cmake/LMDB.cmake b/cmake/LMDB.cmake deleted file mode 100644 index 2d8184de..00000000 --- a/cmake/LMDB.cmake +++ /dev/null @@ -1,29 +0,0 @@ -# -# Find the lmdb library & include dir. -# Build lmdb on Appveyor. -# - -if(APPVEYOR_BUILD) - set(LMDB_VERSION "LMDB_0.9.21") - set(NTDLIB "C:/WINDDK/7600.16385.1/lib/win7/amd64/ntdll.lib") - - execute_process( - COMMAND git clone --depth=1 --branch ${LMDB_VERSION} https://github.com/LMDB/lmdb) - - set(LMDB_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/lmdb/libraries/liblmdb) - - add_library(lmdb - ${CMAKE_SOURCE_DIR}/lmdb/libraries/liblmdb/lmdb.h - ${CMAKE_SOURCE_DIR}/lmdb/libraries/liblmdb/mdb.c - ${CMAKE_SOURCE_DIR}/lmdb/libraries/liblmdb/midl.h - ${CMAKE_SOURCE_DIR}/lmdb/libraries/liblmdb/midl.c) - - set(LMDB_LIBRARY lmdb) -else() - find_path (LMDB_INCLUDE_DIR NAMES lmdb.h PATHS "$ENV{LMDB_DIR}/include") - find_library (LMDB_LIBRARY NAMES lmdb PATHS "$ENV{LMDB_DIR}/lib" ) - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(LMDB DEFAULT_MSG LMDB_INCLUDE_DIR LMDB_LIBRARY) -endif() - -include_directories(${LMDB_INCLUDE_DIR}) diff --git a/cmake/Translations.cmake b/cmake/Translations.cmake index 8ca91883..16120219 100644 --- a/cmake/Translations.cmake +++ b/cmake/Translations.cmake @@ -21,4 +21,8 @@ if(NOT EXISTS ${_qrc}) endif() qt5_add_resources(LANG_QRC ${_qrc}) -qt5_add_resources(QRC resources/res.qrc) +if(Qt5QuickCompiler_FOUND) + qtquick_compiler_add_resources(QRC resources/res.qrc) +else() + qt5_add_resources(QRC resources/res.qrc) +endif() diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt deleted file mode 100644 index 6f8f2616..00000000 --- a/deps/CMakeLists.txt +++ /dev/null @@ -1,125 +0,0 @@ -cmake_minimum_required(VERSION 3.11) -project(NHEKO_DEPS) - -# Point CMake at any custom modules we may ship -list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") - -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -set(DEPS_INSTALL_DIR "${CMAKE_BINARY_DIR}/usr" - CACHE PATH "Dependencies install directory.") -set(DEPS_BIN_DIR "${DEPS_INSTALL_DIR}/bin" - CACHE PATH "Dependencies binary install directory.") -set(DEPS_LIB_DIR "${DEPS_INSTALL_DIR}/lib" - CACHE PATH "Dependencies library install directory.") -set(DEPS_BUILD_DIR "${CMAKE_BINARY_DIR}/build" - CACHE PATH "Dependencies build directory.") -set(DEPS_DOWNLOAD_DIR "${DEPS_BUILD_DIR}/downloads" - CACHE PATH "Dependencies download directory.") - -option(USE_BUNDLED "Use bundled dependencies." ON) - -option(USE_BUNDLED_BOOST "Use the bundled version of Boost." ${USE_BUNDLED}) -option(USE_BUNDLED_CMARK "Use the bundled version of cmark." ${USE_BUNDLED}) -option(USE_BUNDLED_SPDLOG "Use the bundled version of spdlog." ${USE_BUNDLED}) -option(USE_BUNDLED_OLM "Use the bundled version of libolm." ${USE_BUNDLED}) -option(USE_BUNDLED_TWEENY "Use the bundled version of Tweeny." ${USE_BUNDLED}) -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}) -option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json." ${USE_BUNDLED}) -option(MTX_STATIC "Compile / link bundled mtx client statically" OFF) - -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 - https://dl.bintray.com/boostorg/release/1.69.0/source/boost_1_69_0.tar.bz2) -set(BOOST_SHA256 - 8f32d4617390d1c2d16f26a27ab60d97807b35440d45891fa340fc2648b04406) - -set( - MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/975ce8906c42742dbb698fcf9fa15663c530df20.tar.gz) -set(MTXCLIENT_HASH - 5e3169ef19b6e585069ceced42489574ce18380480628339bac015759fa1893e) -set( - TWEENY_URL - https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz - ) -set(TWEENY_HASH - 9a632b9da84823fae002ad5d9ba02c8d77c0a3810479974c6b637c5504165475) - -set( - LMDBXX_HEADER_URL - https://raw.githubusercontent.com/bendiken/lmdbxx/0b43ca87d8cfabba392dfe884eb1edb83874de02/lmdb%2B%2B.h - ) -set(LMDBXX_HASH - c57b501a4e8fa1187fa7fd348da415c7685a50a7cb25b17b3f257b9e9426f73d) - -set(OLM_URL https://gitlab.matrix.org/matrix-org/olm.git) -set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae) - -set(CMARK_URL https://github.com/commonmark/cmark/archive/0.28.3.tar.gz) -set(CMARK_HASH acc98685d3c1b515ff787ac7c994188dadaf28a2d700c10c1221da4199bae1fc) - -set(SPDLOG_URL https://github.com/gabime/spdlog/archive/v1.1.0.tar.gz) -set(SPDLOG_HASH - 3dbcbfd8c07e25f5e0d662b194d3a7772ef214358c49ada23c044c4747ce8b19) - -set(JSON_URL - https://github.com/nlohmann/json.git) -set(JSON_TAG - v3.2.0) - -if(USE_BUNDLED_JSON) - include(Json) -endif() - -if(USE_BUNDLED_BOOST) - include(Boost) -endif() - -if(USE_BUNDLED_SPDLOG) - include(SpdLog) -endif() - -if(USE_BUNDLED_OLM) - include(Olm) -endif() - -if(USE_BUNDLED_CMARK) - include(cmark) -endif() - -if(USE_BUNDLED_TWEENY) - include(Tweeny) -endif() - -if(USE_BUNDLED_LMDBXX) - file(DOWNLOAD ${LMDBXX_HEADER_URL} ${DEPS_INSTALL_DIR}/include/lmdb++.h - EXPECTED_HASH SHA256=${LMDBXX_HASH}) -endif() - -if(WIN32) - if("${TARGET_ARCH}" STREQUAL "X86_64") - set(TARGET_ARCH x64) - elseif(TARGET_ARCH STREQUAL "X86") - set(TARGET_ARCH ia32) - endif() -endif() - -add_custom_target(third-party ALL - COMMAND ${CMAKE_COMMAND} -E touch .third-party - DEPENDS ${THIRD_PARTY_DEPS}) - -if(USE_BUNDLED_MATRIX_CLIENT) - include(MatrixClient) - add_dependencies(MatrixClient third-party) -endif() diff --git a/deps/cmake/Boost.cmake b/deps/cmake/Boost.cmake deleted file mode 100644 index 47eb723b..00000000 --- a/deps/cmake/Boost.cmake +++ /dev/null @@ -1,23 +0,0 @@ -if(WIN32) - message(STATUS "Building Boost in Windows is not supported (skipping)") - return() -endif() - -ExternalProject_Add( - Boost - - URL ${BOOST_URL} - URL_HASH SHA256=${BOOST_SHA256} - DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/boost - DOWNLOAD_NO_PROGRESS 0 - - BUILD_IN_SOURCE 1 - SOURCE_DIR ${DEPS_BUILD_DIR}/boost - CONFIGURE_COMMAND ${DEPS_BUILD_DIR}/boost/bootstrap.sh - --with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex - --prefix=${DEPS_INSTALL_DIR} - BUILD_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 cxxstd=14 variant=release link=shared runtime-link=shared threading=multi --layout=system - INSTALL_COMMAND ${DEPS_BUILD_DIR}/boost/b2 -d0 install -) - -list(APPEND THIRD_PARTY_DEPS Boost) diff --git a/deps/cmake/Json.cmake b/deps/cmake/Json.cmake deleted file mode 100644 index 3b63550e..00000000 --- a/deps/cmake/Json.cmake +++ /dev/null @@ -1,19 +0,0 @@ -ExternalProject_Add( - Json - - GIT_REPOSITORY ${JSON_URL} - GIT_TAG ${JSON_TAG} - - BUILD_IN_SOURCE 1 - SOURCE_DIR ${DEPS_BUILD_DIR}/json - - CONFIGURE_COMMAND ${CMAKE_COMMAND} - -DJSON_BuildTests=OFF - -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} - -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} - - BUILD_COMMAND ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/json - INSTALL_COMMAND make install -) - -list(APPEND THIRD_PARTY_DEPS Json) diff --git a/deps/cmake/MatrixClient.cmake b/deps/cmake/MatrixClient.cmake deleted file mode 100644 index 44992c0b..00000000 --- a/deps/cmake/MatrixClient.cmake +++ /dev/null @@ -1,43 +0,0 @@ -set(PLATFORM_FLAGS "") - -if(MSVC) - set(PLATFORM_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") -endif() - -if(APPLE) - set(PLATFORM_FLAGS "-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl") -endif() - -# Force to build with the bundled version of Boost. This is necessary because -# if an outdated version of Boost is installed, then CMake will grab that -# instead of the bundled version of Boost, like we wanted. -set(BOOST_BUNDLE_ROOT "-DBOOST_ROOT=${DEPS_BUILD_DIR}/boost") - -set (MTX_SHARED ON) - -if (MTX_STATIC) - set (MTX_SHARED OFF) -endif() - -ExternalProject_Add( - MatrixClient - - URL ${MTXCLIENT_URL} - URL_HASH SHA256=${MTXCLIENT_HASH} - - BUILD_IN_SOURCE 1 - SOURCE_DIR ${DEPS_BUILD_DIR}/mtxclient - CONFIGURE_COMMAND ${CMAKE_COMMAND} - -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} - -DCMAKE_BUILD_TYPE=Release - -DBUILD_LIB_TESTS=OFF - -DBUILD_LIB_EXAMPLES=OFF - -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} - ${BOOST_BUNDLE_ROOT} - -DBUILD_SHARED_LIBS=${MTX_SHARED} - ${PLATFORM_FLAGS} - ${DEPS_BUILD_DIR}/mtxclient - BUILD_COMMAND - ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/mtxclient --config Release) - -list(APPEND THIRD_PARTY_DEPS MatrixClient) diff --git a/deps/cmake/Olm.cmake b/deps/cmake/Olm.cmake deleted file mode 100644 index a5b8be76..00000000 --- a/deps/cmake/Olm.cmake +++ /dev/null @@ -1,34 +0,0 @@ -set(WINDOWS_FLAGS "") - -if(MSVC) - set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") -endif() - -ExternalProject_Add( - Olm - - GIT_REPOSITORY ${OLM_URL} - GIT_TAG ${OLM_TAG} - - BUILD_IN_SOURCE 1 - SOURCE_DIR ${DEPS_BUILD_DIR}/olm - CONFIGURE_COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/OlmCMakeLists.txt - ${DEPS_BUILD_DIR}/olm/CMakeLists.txt - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/OlmConfig.cmake.in - ${DEPS_BUILD_DIR}/olm/cmake/OlmConfig.cmake.in - COMMAND ${CMAKE_COMMAND} - -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} - -DCMAKE_BUILD_TYPE=Release - ${DEPS_BUILD_DIR}/olm - ${WINDOWS_FLAGS} - BUILD_COMMAND ${CMAKE_COMMAND} - --build ${DEPS_BUILD_DIR}/olm - --config Release - INSTALL_COMMAND ${CMAKE_COMMAND} - --build ${DEPS_BUILD_DIR}/olm - --config Release - --target install) - -list(APPEND THIRD_PARTY_DEPS Olm) diff --git a/deps/cmake/OlmCMakeLists.txt b/deps/cmake/OlmCMakeLists.txt deleted file mode 100644 index 529cbb92..00000000 --- a/deps/cmake/OlmCMakeLists.txt +++ /dev/null @@ -1,107 +0,0 @@ -cmake_minimum_required(VERSION 3.1) - -project(olm VERSION 2.2.2 LANGUAGES CXX C) - -add_definitions(-DOLMLIB_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}) -add_definitions(-DOLMLIB_VERSION_MINOR=${PROJECT_VERSION_MINOR}) -add_definitions(-DOLMLIB_VERSION_PATCH=${PROJECT_VERSION_PATCH}) - -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) -set(CMAKE_CXX_STANDARD 11) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_C_STANDARD 99) -set(CMAKE_C_STANDARD_REQUIRED ON) -set(CMAKE_POSITION_INDEPENDENT_CODE ON) - -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -add_library(olm - src/account.cpp - src/base64.cpp - src/cipher.cpp - src/crypto.cpp - src/memory.cpp - src/message.cpp - src/pickle.cpp - src/ratchet.cpp - src/session.cpp - src/utility.cpp - - src/ed25519.c - src/error.c - src/inbound_group_session.c - src/megolm.c - src/olm.cpp - src/outbound_group_session.c - src/pickle_encoding.c - - lib/crypto-algorithms/aes.c - lib/crypto-algorithms/sha256.c - lib/curve25519-donna/curve25519-donna.c) -add_library(Olm::Olm ALIAS olm) - -target_include_directories(olm - PUBLIC - $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include> - $<INSTALL_INTERFACE:include> - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/lib) - -set_target_properties(olm PROPERTIES - SOVERSION ${PROJECT_VERSION_MAJOR} - VERSION ${PROJECT_VERSION}) - -set_target_properties(olm PROPERTIES - ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR} - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR} - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) - -# -# Installation -# -include(GNUInstallDirs) -set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/Olm) -install(TARGETS olm - EXPORT olm-targets - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) - -# The exported target will be named Olm. -set_target_properties(olm PROPERTIES EXPORT_NAME Olm) -install(FILES - ${CMAKE_SOURCE_DIR}/include/olm/olm.h - ${CMAKE_SOURCE_DIR}/include/olm/outbound_group_session.h - ${CMAKE_SOURCE_DIR}/include/olm/inbound_group_session.h - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/olm) - -# Export the targets to a script. -install(EXPORT olm-targets - FILE OlmTargets.cmake - NAMESPACE Olm:: - DESTINATION ${INSTALL_CONFIGDIR}) - -# Create a ConfigVersion.cmake file. -include(CMakePackageConfigHelpers) -write_basic_package_version_file( - ${CMAKE_CURRENT_BINARY_DIR}/OlmConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion) - -configure_package_config_file( - ${CMAKE_CURRENT_LIST_DIR}/cmake/OlmConfig.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/OlmConfig.cmake - INSTALL_DESTINATION ${INSTALL_CONFIGDIR}) - -#Install the config & configversion. -install(FILES - ${CMAKE_CURRENT_BINARY_DIR}/OlmConfig.cmake - ${CMAKE_CURRENT_BINARY_DIR}/OlmConfigVersion.cmake - DESTINATION ${INSTALL_CONFIGDIR}) - -# Register package in user's package registry -export(EXPORT olm-targets - FILE ${CMAKE_CURRENT_BINARY_DIR}/OlmTargets.cmake - NAMESPACE Olm::) -export(PACKAGE Olm) diff --git a/deps/cmake/OlmConfig.cmake.in b/deps/cmake/OlmConfig.cmake.in deleted file mode 100644 index b670fe85..00000000 --- a/deps/cmake/OlmConfig.cmake.in +++ /dev/null @@ -1,11 +0,0 @@ -get_filename_component(Olm_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) -include(CMakeFindDependencyMacro) - -list(APPEND CMAKE_MODULE_PATH ${Olm_CMAKE_DIR}) -list(REMOVE_AT CMAKE_MODULE_PATH -1) - -if(NOT TARGET Olm::Olm) - include("${Olm_CMAKE_DIR}/OlmTargets.cmake") -endif() - -set(Olm_LIBRARIES Olm::Olm) diff --git a/deps/cmake/SpdLog.cmake b/deps/cmake/SpdLog.cmake deleted file mode 100644 index 27109b66..00000000 --- a/deps/cmake/SpdLog.cmake +++ /dev/null @@ -1,23 +0,0 @@ -set(WINDOWS_FLAGS "") - -if(MSVC) - set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") -endif() - -ExternalProject_Add( - SpdLog - - URL ${SPDLOG_URL} - URL_HASH SHA256=${SPDLOG_HASH} - - BUILD_IN_SOURCE 1 - SOURCE_DIR ${DEPS_BUILD_DIR}/spdlog - CONFIGURE_COMMAND ${CMAKE_COMMAND} - -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} - -DSPDLOG_BUILD_EXAMPLES=0 - -DSPDLOG_BUILD_BENCH=0 - -DSPDLOG_BUILD_TESTING=0 - ${DEPS_BUILD_DIR}/spdlog - ${WINDOWS_FLAGS}) - -list(APPEND THIRD_PARTY_DEPS SpdLog) diff --git a/deps/cmake/Tweeny.cmake b/deps/cmake/Tweeny.cmake deleted file mode 100644 index 5a4303f9..00000000 --- a/deps/cmake/Tweeny.cmake +++ /dev/null @@ -1,22 +0,0 @@ -set(WINDOWS_FLAGS "") - -if(MSVC) - set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") -endif() - -ExternalProject_Add( - Tweeny - - URL ${TWEENY_URL} - URL_HASH SHA256=${TWEENY_HASH} - - BUILD_IN_SOURCE 1 - SOURCE_DIR ${DEPS_BUILD_DIR}/tweeny - CONFIGURE_COMMAND ${CMAKE_COMMAND} - -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} - -DTWEENY_BUILD_EXAMPLES=OFF - -DTWEENY_BUILD_DOCUMENTATION=OFF - ${DEPS_BUILD_DIR}/tweeny - ${WINDOWS_FLAGS}) - -list(APPEND THIRD_PARTY_DEPS Tweeny) diff --git a/deps/cmake/cmark.cmake b/deps/cmake/cmark.cmake deleted file mode 100644 index e0d45e88..00000000 --- a/deps/cmake/cmark.cmake +++ /dev/null @@ -1,21 +0,0 @@ -set(WINDOWS_FLAGS "") - -if(MSVC) - set(WINDOWS_FLAGS "-DCMAKE_GENERATOR_PLATFORM=x64") -endif() - -ExternalProject_Add( - cmark - - URL ${CMARK_URL} - URL_HASH SHA256=${CMARK_HASH} - - BUILD_IN_SOURCE 0 - SOURCE_DIR ${DEPS_BUILD_DIR}/cmark - CONFIGURE_COMMAND ${CMAKE_COMMAND} - -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} - -DCMARK_TESTS=OFF - ${DEPS_BUILD_DIR}/cmark - ${WINDOWS_FLAGS}) - -list(APPEND THIRD_PARTY_DEPS cmark) diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json new file mode 100644 index 00000000..ddc1f1a0 --- /dev/null +++ b/io.github.NhekoReborn.Nheko.json @@ -0,0 +1,194 @@ +{ + "id": "io.github.NhekoReborn.Nheko", + "command": "nheko", + "branch": "0.7.0-dev", + "runtime": "org.kde.Platform", + "runtime-version": "5.14", + "sdk": "org.kde.Sdk", + "rename-icon": "nheko", + "rename-desktop-file": "nheko.desktop", + "rename-appdata-file": "nheko.appdata.xml", + "finish-args": [ + "--device=dri", + "--filesystem=home", + "--share=ipc", + "--share=network", + "--socket=pulseaudio", + "--socket=wayland", + "--socket=x11", + "--talk-name=org.freedesktop.Notifications", + "--talk-name=org.kde.StatusNotifierWatcher" + ], + "cleanup": [ + "/include", + "/bin/mdb*", + "*.a" + ], + "build-options" : { + "arch": { + "aarch64": { + "cxxflags": "-DBOOST_ASIO_DISABLE_EPOLL" + } + } + }, + "modules": [ + { + "name": "lmdb", + "sources": [ + { + "sha256": "f3927859882eb608868c8c31586bb7eb84562a40a6bf5cc3e13b6b564641ea28", + "type": "archive", + "url": "https://github.com/LMDB/lmdb/archive/LMDB_0.9.22.tar.gz" + } + ], + "make-install-args": [ + "prefix=/app" + ], + "no-autogen": true, + "subdir": "libraries/liblmdb" + }, + { + "name": "cmark", + "buildsystem": "cmake-ninja", + "builddir": true, + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release", + "-DCMARK_TESTS=OFF" + ], + "sources": [ + { + "sha256": "2558ace3cbeff85610de3bda32858f722b359acdadf0c4691851865bb84924a6", + "type": "archive", + "url": "https://github.com/commonmark/cmark/archive/0.29.0.tar.gz" + } + ] + }, + { + "name": "spdlog", + "buildsystem": "cmake-ninja", + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release", + "-DSPDLOG_BUILD_EXAMPLES=0", + "-DSPDLOG_BUILD_BENCH=0", + "-DSPDLOG_BUILD_TESTING=0" + ], + "sources": [ + { + "sha256": "3dbcbfd8c07e25f5e0d662b194d3a7772ef214358c49ada23c044c4747ce8b19", + "type": "archive", + "url": "https://github.com/gabime/spdlog/archive/v1.1.0.tar.gz" + } + ] + }, + { + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release" + ], + "buildsystem": "cmake-ninja", + "name": "olm", + "sources": [ + { + "commit": "6753595300767dd70150831dbbe6f92d64e75038", + "disable-shallow-clone": true, + "tag": "3.1.4", + "type": "git", + "url": "https://gitlab.matrix.org/matrix-org/olm.git" + } + ] + }, + { + "config-opts":[ + "-DJSON_BuildTests=OFF" + ], + "buildsystem":"cmake", + "name": "nlohmann", + "sources":[ + { + "sha256": "d51a3a8d3efbb1139d7608e28782ea9efea7e7933157e8ff8184901efd8ee760", + "type": "archive", + "url": "https://github.com/nlohmann/json/archive/v3.7.0.tar.gz" + } + ] + }, + { + "name": "sodium", + "sources": [ + { + "sha256": "6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e46636c1", + "type": "archive", + "url": "https://github.com/jedisct1/libsodium/releases/download/1.0.18-RELEASE/libsodium-1.0.18.tar.gz" + } + ] + }, + { + "build-commands": [ + "./bootstrap.sh --with-libraries=thread,system,iostreams --prefix=/app", + "./b2 -d0 variant=release link=static threading=multi --layout=system", + "./b2 -d0 install" + ], + "buildsystem": "simple", + "name": "boost", + "sources": [ + { + "sha256": "59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722", + "type": "archive", + "url": "https://dl.bintray.com/boostorg/release/1.72.0/source/boost_1_72_0.tar.bz2" + } + ] + }, + { + "config-opts": [ + "-DBUILD_LIB_TESTS=OFF", + "-DBUILD_LIB_EXAMPLES=OFF", + "-DCMAKE_BUILD_TYPE=Release", + "-DBUILD_SHARED_LIBS=OFF" + ], + "buildsystem": "cmake-ninja", + "name": "mtxclient", + "sources": [ + { + "sha256": "df3fe7e3d59b5fc52ee3ca9a132a55fc325aa799c676e9e420073c56daeb1848", + "type": "archive", + "url": "https://github.com/Nheko-Reborn/mtxclient/archive/5838f607d0e4c7595439249e8b9c213aec0667e9.tar.gz" + } + ] + }, + { + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release", + "-DTWEENY_BUILD_DOCUMENTATION=OFF", + "-DTWEENY_BUILD_EXAMPLES=OFF" + ], + "buildsystem": "cmake-ninja", + "name": "tweeny", + "sources": [ + { + "sha256": "482857256a7235646004682912badb6521d361ed6987c8ebdae7986bf64ce694", + "type": "archive", + "url": "https://github.com/mobius3/tweeny/archive/43f4130f7e4a67c19d870b60864bc2862c19b81f.tar.gz" + } + ] + }, + { + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release", + "-DLMDBXX_INCLUDE_DIR=.deps/lmdbxx" + ], + "buildsystem": "cmake-ninja", + "name": "nheko", + "sources": [ + { + "path": ".", + "type": "dir", + "skip": ["build-flatpak"] + }, + { + "dest": ".deps/lmdbxx", + "sha256": "93721132bbf5045d38ad62de2997655e9984c48ea5c9886746d42128f4b26fbd", + "type": "archive", + "url": "https://github.com/bendiken/lmdbxx/archive/0b43ca87d8cfabba392dfe884eb1edb83874de02.tar.gz" + } + ] + } + ] +} diff --git a/resources/emoji-test.txt b/resources/emoji-test.txt new file mode 100644 index 00000000..cadaf6cb --- /dev/null +++ b/resources/emoji-test.txt @@ -0,0 +1,4446 @@ +# emoji-test.txt +# Date: 2019-11-07, 19:33:54 GMT +# © 2019 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Emoji Keyboard/Display Test Data for UTS #51 +# Version: 13.0 +# +# For documentation and usage, see http://www.unicode.org/reports/tr51 +# +# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed. +# Format: code points; status # emoji name +# Code points — list of one or more hex code points, separated by spaces +# Status +# component — an Emoji_Component, +# excluding Regional_Indicators, ASCII, and non-Emoji. +# fully-qualified — a fully-qualified emoji (see ED-18 in UTS #51), +# excluding Emoji_Component +# minimally-qualified — a minimally-qualified emoji (see ED-18a in UTS #51) +# unqualified — a unqualified emoji (See ED-19 in UTS #51) +# Notes: +# • This includes the emoji components that need emoji presentation (skin tone and hair) +# when isolated, but omits the components that need not have an emoji +# presentation when isolated. +# • The RGI set is covered by the listed fully-qualified emoji. +# • The listed minimally-qualified and unqualified cover all cases where an +# element of the RGI set is missing one or more emoji presentation selectors. +# • The file is in CLDR order, not codepoint order. This is recommended (but not required!) for keyboard palettes. +# • The groups and subgroups are illustrative. See the Emoji Order chart for more information. + + +# group: Smileys & Emotion + +# subgroup: face-smiling +1F600 ; fully-qualified # 😀 E1.0 grinning face +1F603 ; fully-qualified # 😃 E0.6 grinning face with big eyes +1F604 ; fully-qualified # 😄 E0.6 grinning face with smiling eyes +1F601 ; fully-qualified # 😁 E0.6 beaming face with smiling eyes +1F606 ; fully-qualified # 😆 E0.6 grinning squinting face +1F605 ; fully-qualified # 😅 E0.6 grinning face with sweat +1F923 ; fully-qualified # 🤣 E3.0 rolling on the floor laughing +1F602 ; fully-qualified # 😂 E0.6 face with tears of joy +1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face +1F643 ; fully-qualified # 🙃 E1.0 upside-down face +1F609 ; fully-qualified # 😉 E0.6 winking face +1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes +1F607 ; fully-qualified # 😇 E1.0 smiling face with halo + +# subgroup: face-affection +1F970 ; fully-qualified # 🥰 E11.0 smiling face with hearts +1F60D ; fully-qualified # 😍 E0.6 smiling face with heart-eyes +1F929 ; fully-qualified # 🤩 E5.0 star-struck +1F618 ; fully-qualified # 😘 E0.6 face blowing a kiss +1F617 ; fully-qualified # 😗 E1.0 kissing face +263A FE0F ; fully-qualified # ☺️ E0.6 smiling face +263A ; unqualified # ☺ E0.6 smiling face +1F61A ; fully-qualified # 😚 E0.6 kissing face with closed eyes +1F619 ; fully-qualified # 😙 E1.0 kissing face with smiling eyes +1F972 ; fully-qualified # 🥲 E13.0 smiling face with tear + +# subgroup: face-tongue +1F60B ; fully-qualified # 😋 E0.6 face savoring food +1F61B ; fully-qualified # 😛 E1.0 face with tongue +1F61C ; fully-qualified # 😜 E0.6 winking face with tongue +1F92A ; fully-qualified # 🤪 E5.0 zany face +1F61D ; fully-qualified # 😝 E0.6 squinting face with tongue +1F911 ; fully-qualified # 🤑 E1.0 money-mouth face + +# subgroup: face-hand +1F917 ; fully-qualified # 🤗 E1.0 hugging face +1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth +1F92B ; fully-qualified # 🤫 E5.0 shushing face +1F914 ; fully-qualified # 🤔 E1.0 thinking face + +# subgroup: face-neutral-skeptical +1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face +1F928 ; fully-qualified # 🤨 E5.0 face with raised eyebrow +1F610 ; fully-qualified # 😐 E0.7 neutral face +1F611 ; fully-qualified # 😑 E1.0 expressionless face +1F636 ; fully-qualified # 😶 E1.0 face without mouth +1F60F ; fully-qualified # 😏 E0.6 smirking face +1F612 ; fully-qualified # 😒 E0.6 unamused face +1F644 ; fully-qualified # 🙄 E1.0 face with rolling eyes +1F62C ; fully-qualified # 😬 E1.0 grimacing face +1F925 ; fully-qualified # 🤥 E3.0 lying face + +# subgroup: face-sleepy +1F60C ; fully-qualified # 😌 E0.6 relieved face +1F614 ; fully-qualified # 😔 E0.6 pensive face +1F62A ; fully-qualified # 😪 E0.6 sleepy face +1F924 ; fully-qualified # 🤤 E3.0 drooling face +1F634 ; fully-qualified # 😴 E1.0 sleeping face + +# subgroup: face-unwell +1F637 ; fully-qualified # 😷 E0.6 face with medical mask +1F912 ; fully-qualified # 🤒 E1.0 face with thermometer +1F915 ; fully-qualified # 🤕 E1.0 face with head-bandage +1F922 ; fully-qualified # 🤢 E3.0 nauseated face +1F92E ; fully-qualified # 🤮 E5.0 face vomiting +1F927 ; fully-qualified # 🤧 E3.0 sneezing face +1F975 ; fully-qualified # 🥵 E11.0 hot face +1F976 ; fully-qualified # 🥶 E11.0 cold face +1F974 ; fully-qualified # 🥴 E11.0 woozy face +1F635 ; fully-qualified # 😵 E0.6 dizzy face +1F92F ; fully-qualified # 🤯 E5.0 exploding head + +# subgroup: face-hat +1F920 ; fully-qualified # 🤠 E3.0 cowboy hat face +1F973 ; fully-qualified # 🥳 E11.0 partying face +1F978 ; fully-qualified # 🥸 E13.0 disguised face + +# subgroup: face-glasses +1F60E ; fully-qualified # 😎 E1.0 smiling face with sunglasses +1F913 ; fully-qualified # 🤓 E1.0 nerd face +1F9D0 ; fully-qualified # 🧐 E5.0 face with monocle + +# subgroup: face-concerned +1F615 ; fully-qualified # 😕 E1.0 confused face +1F61F ; fully-qualified # 😟 E1.0 worried face +1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face +2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face +2639 ; unqualified # ☹ E0.7 frowning face +1F62E ; fully-qualified # 😮 E1.0 face with open mouth +1F62F ; fully-qualified # 😯 E1.0 hushed face +1F632 ; fully-qualified # 😲 E0.6 astonished face +1F633 ; fully-qualified # 😳 E0.6 flushed face +1F97A ; fully-qualified # 🥺 E11.0 pleading face +1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth +1F627 ; fully-qualified # 😧 E1.0 anguished face +1F628 ; fully-qualified # 😨 E0.6 fearful face +1F630 ; fully-qualified # 😰 E0.6 anxious face with sweat +1F625 ; fully-qualified # 😥 E0.6 sad but relieved face +1F622 ; fully-qualified # 😢 E0.6 crying face +1F62D ; fully-qualified # 😭 E0.6 loudly crying face +1F631 ; fully-qualified # 😱 E0.6 face screaming in fear +1F616 ; fully-qualified # 😖 E0.6 confounded face +1F623 ; fully-qualified # 😣 E0.6 persevering face +1F61E ; fully-qualified # 😞 E0.6 disappointed face +1F613 ; fully-qualified # 😓 E0.6 downcast face with sweat +1F629 ; fully-qualified # 😩 E0.6 weary face +1F62B ; fully-qualified # 😫 E0.6 tired face +1F971 ; fully-qualified # 🥱 E12.0 yawning face + +# subgroup: face-negative +1F624 ; fully-qualified # 😤 E0.6 face with steam from nose +1F621 ; fully-qualified # 😡 E0.6 pouting face +1F620 ; fully-qualified # 😠 E0.6 angry face +1F92C ; fully-qualified # 🤬 E5.0 face with symbols on mouth +1F608 ; fully-qualified # 😈 E1.0 smiling face with horns +1F47F ; fully-qualified # 👿 E0.6 angry face with horns +1F480 ; fully-qualified # 💀 E0.6 skull +2620 FE0F ; fully-qualified # ☠️ E1.0 skull and crossbones +2620 ; unqualified # ☠ E1.0 skull and crossbones + +# subgroup: face-costume +1F4A9 ; fully-qualified # 💩 E0.6 pile of poo +1F921 ; fully-qualified # 🤡 E3.0 clown face +1F479 ; fully-qualified # 👹 E0.6 ogre +1F47A ; fully-qualified # 👺 E0.6 goblin +1F47B ; fully-qualified # 👻 E0.6 ghost +1F47D ; fully-qualified # 👽 E0.6 alien +1F47E ; fully-qualified # 👾 E0.6 alien monster +1F916 ; fully-qualified # 🤖 E1.0 robot + +# subgroup: cat-face +1F63A ; fully-qualified # 😺 E0.6 grinning cat +1F638 ; fully-qualified # 😸 E0.6 grinning cat with smiling eyes +1F639 ; fully-qualified # 😹 E0.6 cat with tears of joy +1F63B ; fully-qualified # 😻 E0.6 smiling cat with heart-eyes +1F63C ; fully-qualified # 😼 E0.6 cat with wry smile +1F63D ; fully-qualified # 😽 E0.6 kissing cat +1F640 ; fully-qualified # 🙀 E0.6 weary cat +1F63F ; fully-qualified # 😿 E0.6 crying cat +1F63E ; fully-qualified # 😾 E0.6 pouting cat + +# subgroup: monkey-face +1F648 ; fully-qualified # 🙈 E0.6 see-no-evil monkey +1F649 ; fully-qualified # 🙉 E0.6 hear-no-evil monkey +1F64A ; fully-qualified # 🙊 E0.6 speak-no-evil monkey + +# subgroup: emotion +1F48B ; fully-qualified # 💋 E0.6 kiss mark +1F48C ; fully-qualified # 💌 E0.6 love letter +1F498 ; fully-qualified # 💘 E0.6 heart with arrow +1F49D ; fully-qualified # 💝 E0.6 heart with ribbon +1F496 ; fully-qualified # 💖 E0.6 sparkling heart +1F497 ; fully-qualified # 💗 E0.6 growing heart +1F493 ; fully-qualified # 💓 E0.6 beating heart +1F49E ; fully-qualified # 💞 E0.6 revolving hearts +1F495 ; fully-qualified # 💕 E0.6 two hearts +1F49F ; fully-qualified # 💟 E0.6 heart decoration +2763 FE0F ; fully-qualified # ❣️ E1.0 heart exclamation +2763 ; unqualified # ❣ E1.0 heart exclamation +1F494 ; fully-qualified # 💔 E0.6 broken heart +2764 FE0F ; fully-qualified # ❤️ E0.6 red heart +2764 ; unqualified # ❤ E0.6 red heart +1F9E1 ; fully-qualified # 🧡 E5.0 orange heart +1F49B ; fully-qualified # 💛 E0.6 yellow heart +1F49A ; fully-qualified # 💚 E0.6 green heart +1F499 ; fully-qualified # 💙 E0.6 blue heart +1F49C ; fully-qualified # 💜 E0.6 purple heart +1F90E ; fully-qualified # 🤎 E12.0 brown heart +1F5A4 ; fully-qualified # 🖤 E3.0 black heart +1F90D ; fully-qualified # 🤍 E12.0 white heart +1F4AF ; fully-qualified # 💯 E0.6 hundred points +1F4A2 ; fully-qualified # 💢 E0.6 anger symbol +1F4A5 ; fully-qualified # 💥 E0.6 collision +1F4AB ; fully-qualified # 💫 E0.6 dizzy +1F4A6 ; fully-qualified # 💦 E0.6 sweat droplets +1F4A8 ; fully-qualified # 💨 E0.6 dashing away +1F573 FE0F ; fully-qualified # 🕳️ E0.7 hole +1F573 ; unqualified # 🕳 E0.7 hole +1F4A3 ; fully-qualified # 💣 E0.6 bomb +1F4AC ; fully-qualified # 💬 E0.6 speech balloon +1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️🗨️ E2.0 eye in speech bubble +1F441 200D 1F5E8 FE0F ; unqualified # 👁🗨️ E2.0 eye in speech bubble +1F441 FE0F 200D 1F5E8 ; unqualified # 👁️🗨 E2.0 eye in speech bubble +1F441 200D 1F5E8 ; unqualified # 👁🗨 E2.0 eye in speech bubble +1F5E8 FE0F ; fully-qualified # 🗨️ E2.0 left speech bubble +1F5E8 ; unqualified # 🗨 E2.0 left speech bubble +1F5EF FE0F ; fully-qualified # 🗯️ E0.7 right anger bubble +1F5EF ; unqualified # 🗯 E0.7 right anger bubble +1F4AD ; fully-qualified # 💭 E1.0 thought balloon +1F4A4 ; fully-qualified # 💤 E0.6 zzz + +# Smileys & Emotion subtotal: 162 +# Smileys & Emotion subtotal: 162 w/o modifiers + +# group: People & Body + +# subgroup: hand-fingers-open +1F44B ; fully-qualified # 👋 E0.6 waving hand +1F44B 1F3FB ; fully-qualified # 👋🏻 E1.0 waving hand: light skin tone +1F44B 1F3FC ; fully-qualified # 👋🏼 E1.0 waving hand: medium-light skin tone +1F44B 1F3FD ; fully-qualified # 👋🏽 E1.0 waving hand: medium skin tone +1F44B 1F3FE ; fully-qualified # 👋🏾 E1.0 waving hand: medium-dark skin tone +1F44B 1F3FF ; fully-qualified # 👋🏿 E1.0 waving hand: dark skin tone +1F91A ; fully-qualified # 🤚 E3.0 raised back of hand +1F91A 1F3FB ; fully-qualified # 🤚🏻 E3.0 raised back of hand: light skin tone +1F91A 1F3FC ; fully-qualified # 🤚🏼 E3.0 raised back of hand: medium-light skin tone +1F91A 1F3FD ; fully-qualified # 🤚🏽 E3.0 raised back of hand: medium skin tone +1F91A 1F3FE ; fully-qualified # 🤚🏾 E3.0 raised back of hand: medium-dark skin tone +1F91A 1F3FF ; fully-qualified # 🤚🏿 E3.0 raised back of hand: dark skin tone +1F590 FE0F ; fully-qualified # 🖐️ E0.7 hand with fingers splayed +1F590 ; unqualified # 🖐 E0.7 hand with fingers splayed +1F590 1F3FB ; fully-qualified # 🖐🏻 E1.0 hand with fingers splayed: light skin tone +1F590 1F3FC ; fully-qualified # 🖐🏼 E1.0 hand with fingers splayed: medium-light skin tone +1F590 1F3FD ; fully-qualified # 🖐🏽 E1.0 hand with fingers splayed: medium skin tone +1F590 1F3FE ; fully-qualified # 🖐🏾 E1.0 hand with fingers splayed: medium-dark skin tone +1F590 1F3FF ; fully-qualified # 🖐🏿 E1.0 hand with fingers splayed: dark skin tone +270B ; fully-qualified # ✋ E0.6 raised hand +270B 1F3FB ; fully-qualified # ✋🏻 E1.0 raised hand: light skin tone +270B 1F3FC ; fully-qualified # ✋🏼 E1.0 raised hand: medium-light skin tone +270B 1F3FD ; fully-qualified # ✋🏽 E1.0 raised hand: medium skin tone +270B 1F3FE ; fully-qualified # ✋🏾 E1.0 raised hand: medium-dark skin tone +270B 1F3FF ; fully-qualified # ✋🏿 E1.0 raised hand: dark skin tone +1F596 ; fully-qualified # 🖖 E1.0 vulcan salute +1F596 1F3FB ; fully-qualified # 🖖🏻 E1.0 vulcan salute: light skin tone +1F596 1F3FC ; fully-qualified # 🖖🏼 E1.0 vulcan salute: medium-light skin tone +1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone +1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone +1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone + +# subgroup: hand-fingers-partial +1F44C ; fully-qualified # 👌 E0.6 OK hand +1F44C 1F3FB ; fully-qualified # 👌🏻 E1.0 OK hand: light skin tone +1F44C 1F3FC ; fully-qualified # 👌🏼 E1.0 OK hand: medium-light skin tone +1F44C 1F3FD ; fully-qualified # 👌🏽 E1.0 OK hand: medium skin tone +1F44C 1F3FE ; fully-qualified # 👌🏾 E1.0 OK hand: medium-dark skin tone +1F44C 1F3FF ; fully-qualified # 👌🏿 E1.0 OK hand: dark skin tone +1F90C ; fully-qualified # 🤌 E13.0 pinched fingers +1F90C 1F3FB ; fully-qualified # 🤌🏻 E13.0 pinched fingers: light skin tone +1F90C 1F3FC ; fully-qualified # 🤌🏼 E13.0 pinched fingers: medium-light skin tone +1F90C 1F3FD ; fully-qualified # 🤌🏽 E13.0 pinched fingers: medium skin tone +1F90C 1F3FE ; fully-qualified # 🤌🏾 E13.0 pinched fingers: medium-dark skin tone +1F90C 1F3FF ; fully-qualified # 🤌🏿 E13.0 pinched fingers: dark skin tone +1F90F ; fully-qualified # 🤏 E12.0 pinching hand +1F90F 1F3FB ; fully-qualified # 🤏🏻 E12.0 pinching hand: light skin tone +1F90F 1F3FC ; fully-qualified # 🤏🏼 E12.0 pinching hand: medium-light skin tone +1F90F 1F3FD ; fully-qualified # 🤏🏽 E12.0 pinching hand: medium skin tone +1F90F 1F3FE ; fully-qualified # 🤏🏾 E12.0 pinching hand: medium-dark skin tone +1F90F 1F3FF ; fully-qualified # 🤏🏿 E12.0 pinching hand: dark skin tone +270C FE0F ; fully-qualified # ✌️ E0.6 victory hand +270C ; unqualified # ✌ E0.6 victory hand +270C 1F3FB ; fully-qualified # ✌🏻 E1.0 victory hand: light skin tone +270C 1F3FC ; fully-qualified # ✌🏼 E1.0 victory hand: medium-light skin tone +270C 1F3FD ; fully-qualified # ✌🏽 E1.0 victory hand: medium skin tone +270C 1F3FE ; fully-qualified # ✌🏾 E1.0 victory hand: medium-dark skin tone +270C 1F3FF ; fully-qualified # ✌🏿 E1.0 victory hand: dark skin tone +1F91E ; fully-qualified # 🤞 E3.0 crossed fingers +1F91E 1F3FB ; fully-qualified # 🤞🏻 E3.0 crossed fingers: light skin tone +1F91E 1F3FC ; fully-qualified # 🤞🏼 E3.0 crossed fingers: medium-light skin tone +1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone +1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone +1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone +1F91F ; fully-qualified # 🤟 E5.0 love-you gesture +1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone +1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone +1F91F 1F3FD ; fully-qualified # 🤟🏽 E5.0 love-you gesture: medium skin tone +1F91F 1F3FE ; fully-qualified # 🤟🏾 E5.0 love-you gesture: medium-dark skin tone +1F91F 1F3FF ; fully-qualified # 🤟🏿 E5.0 love-you gesture: dark skin tone +1F918 ; fully-qualified # 🤘 E1.0 sign of the horns +1F918 1F3FB ; fully-qualified # 🤘🏻 E1.0 sign of the horns: light skin tone +1F918 1F3FC ; fully-qualified # 🤘🏼 E1.0 sign of the horns: medium-light skin tone +1F918 1F3FD ; fully-qualified # 🤘🏽 E1.0 sign of the horns: medium skin tone +1F918 1F3FE ; fully-qualified # 🤘🏾 E1.0 sign of the horns: medium-dark skin tone +1F918 1F3FF ; fully-qualified # 🤘🏿 E1.0 sign of the horns: dark skin tone +1F919 ; fully-qualified # 🤙 E3.0 call me hand +1F919 1F3FB ; fully-qualified # 🤙🏻 E3.0 call me hand: light skin tone +1F919 1F3FC ; fully-qualified # 🤙🏼 E3.0 call me hand: medium-light skin tone +1F919 1F3FD ; fully-qualified # 🤙🏽 E3.0 call me hand: medium skin tone +1F919 1F3FE ; fully-qualified # 🤙🏾 E3.0 call me hand: medium-dark skin tone +1F919 1F3FF ; fully-qualified # 🤙🏿 E3.0 call me hand: dark skin tone + +# subgroup: hand-single-finger +1F448 ; fully-qualified # 👈 E0.6 backhand index pointing left +1F448 1F3FB ; fully-qualified # 👈🏻 E1.0 backhand index pointing left: light skin tone +1F448 1F3FC ; fully-qualified # 👈🏼 E1.0 backhand index pointing left: medium-light skin tone +1F448 1F3FD ; fully-qualified # 👈🏽 E1.0 backhand index pointing left: medium skin tone +1F448 1F3FE ; fully-qualified # 👈🏾 E1.0 backhand index pointing left: medium-dark skin tone +1F448 1F3FF ; fully-qualified # 👈🏿 E1.0 backhand index pointing left: dark skin tone +1F449 ; fully-qualified # 👉 E0.6 backhand index pointing right +1F449 1F3FB ; fully-qualified # 👉🏻 E1.0 backhand index pointing right: light skin tone +1F449 1F3FC ; fully-qualified # 👉🏼 E1.0 backhand index pointing right: medium-light skin tone +1F449 1F3FD ; fully-qualified # 👉🏽 E1.0 backhand index pointing right: medium skin tone +1F449 1F3FE ; fully-qualified # 👉🏾 E1.0 backhand index pointing right: medium-dark skin tone +1F449 1F3FF ; fully-qualified # 👉🏿 E1.0 backhand index pointing right: dark skin tone +1F446 ; fully-qualified # 👆 E0.6 backhand index pointing up +1F446 1F3FB ; fully-qualified # 👆🏻 E1.0 backhand index pointing up: light skin tone +1F446 1F3FC ; fully-qualified # 👆🏼 E1.0 backhand index pointing up: medium-light skin tone +1F446 1F3FD ; fully-qualified # 👆🏽 E1.0 backhand index pointing up: medium skin tone +1F446 1F3FE ; fully-qualified # 👆🏾 E1.0 backhand index pointing up: medium-dark skin tone +1F446 1F3FF ; fully-qualified # 👆🏿 E1.0 backhand index pointing up: dark skin tone +1F595 ; fully-qualified # 🖕 E1.0 middle finger +1F595 1F3FB ; fully-qualified # 🖕🏻 E1.0 middle finger: light skin tone +1F595 1F3FC ; fully-qualified # 🖕🏼 E1.0 middle finger: medium-light skin tone +1F595 1F3FD ; fully-qualified # 🖕🏽 E1.0 middle finger: medium skin tone +1F595 1F3FE ; fully-qualified # 🖕🏾 E1.0 middle finger: medium-dark skin tone +1F595 1F3FF ; fully-qualified # 🖕🏿 E1.0 middle finger: dark skin tone +1F447 ; fully-qualified # 👇 E0.6 backhand index pointing down +1F447 1F3FB ; fully-qualified # 👇🏻 E1.0 backhand index pointing down: light skin tone +1F447 1F3FC ; fully-qualified # 👇🏼 E1.0 backhand index pointing down: medium-light skin tone +1F447 1F3FD ; fully-qualified # 👇🏽 E1.0 backhand index pointing down: medium skin tone +1F447 1F3FE ; fully-qualified # 👇🏾 E1.0 backhand index pointing down: medium-dark skin tone +1F447 1F3FF ; fully-qualified # 👇🏿 E1.0 backhand index pointing down: dark skin tone +261D FE0F ; fully-qualified # ☝️ E0.6 index pointing up +261D ; unqualified # ☝ E0.6 index pointing up +261D 1F3FB ; fully-qualified # ☝🏻 E1.0 index pointing up: light skin tone +261D 1F3FC ; fully-qualified # ☝🏼 E1.0 index pointing up: medium-light skin tone +261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone +261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone +261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone + +# subgroup: hand-fingers-closed +1F44D ; fully-qualified # 👍 E0.6 thumbs up +1F44D 1F3FB ; fully-qualified # 👍🏻 E1.0 thumbs up: light skin tone +1F44D 1F3FC ; fully-qualified # 👍🏼 E1.0 thumbs up: medium-light skin tone +1F44D 1F3FD ; fully-qualified # 👍🏽 E1.0 thumbs up: medium skin tone +1F44D 1F3FE ; fully-qualified # 👍🏾 E1.0 thumbs up: medium-dark skin tone +1F44D 1F3FF ; fully-qualified # 👍🏿 E1.0 thumbs up: dark skin tone +1F44E ; fully-qualified # 👎 E0.6 thumbs down +1F44E 1F3FB ; fully-qualified # 👎🏻 E1.0 thumbs down: light skin tone +1F44E 1F3FC ; fully-qualified # 👎🏼 E1.0 thumbs down: medium-light skin tone +1F44E 1F3FD ; fully-qualified # 👎🏽 E1.0 thumbs down: medium skin tone +1F44E 1F3FE ; fully-qualified # 👎🏾 E1.0 thumbs down: medium-dark skin tone +1F44E 1F3FF ; fully-qualified # 👎🏿 E1.0 thumbs down: dark skin tone +270A ; fully-qualified # ✊ E0.6 raised fist +270A 1F3FB ; fully-qualified # ✊🏻 E1.0 raised fist: light skin tone +270A 1F3FC ; fully-qualified # ✊🏼 E1.0 raised fist: medium-light skin tone +270A 1F3FD ; fully-qualified # ✊🏽 E1.0 raised fist: medium skin tone +270A 1F3FE ; fully-qualified # ✊🏾 E1.0 raised fist: medium-dark skin tone +270A 1F3FF ; fully-qualified # ✊🏿 E1.0 raised fist: dark skin tone +1F44A ; fully-qualified # 👊 E0.6 oncoming fist +1F44A 1F3FB ; fully-qualified # 👊🏻 E1.0 oncoming fist: light skin tone +1F44A 1F3FC ; fully-qualified # 👊🏼 E1.0 oncoming fist: medium-light skin tone +1F44A 1F3FD ; fully-qualified # 👊🏽 E1.0 oncoming fist: medium skin tone +1F44A 1F3FE ; fully-qualified # 👊🏾 E1.0 oncoming fist: medium-dark skin tone +1F44A 1F3FF ; fully-qualified # 👊🏿 E1.0 oncoming fist: dark skin tone +1F91B ; fully-qualified # 🤛 E3.0 left-facing fist +1F91B 1F3FB ; fully-qualified # 🤛🏻 E3.0 left-facing fist: light skin tone +1F91B 1F3FC ; fully-qualified # 🤛🏼 E3.0 left-facing fist: medium-light skin tone +1F91B 1F3FD ; fully-qualified # 🤛🏽 E3.0 left-facing fist: medium skin tone +1F91B 1F3FE ; fully-qualified # 🤛🏾 E3.0 left-facing fist: medium-dark skin tone +1F91B 1F3FF ; fully-qualified # 🤛🏿 E3.0 left-facing fist: dark skin tone +1F91C ; fully-qualified # 🤜 E3.0 right-facing fist +1F91C 1F3FB ; fully-qualified # 🤜🏻 E3.0 right-facing fist: light skin tone +1F91C 1F3FC ; fully-qualified # 🤜🏼 E3.0 right-facing fist: medium-light skin tone +1F91C 1F3FD ; fully-qualified # 🤜🏽 E3.0 right-facing fist: medium skin tone +1F91C 1F3FE ; fully-qualified # 🤜🏾 E3.0 right-facing fist: medium-dark skin tone +1F91C 1F3FF ; fully-qualified # 🤜🏿 E3.0 right-facing fist: dark skin tone + +# subgroup: hands +1F44F ; fully-qualified # 👏 E0.6 clapping hands +1F44F 1F3FB ; fully-qualified # 👏🏻 E1.0 clapping hands: light skin tone +1F44F 1F3FC ; fully-qualified # 👏🏼 E1.0 clapping hands: medium-light skin tone +1F44F 1F3FD ; fully-qualified # 👏🏽 E1.0 clapping hands: medium skin tone +1F44F 1F3FE ; fully-qualified # 👏🏾 E1.0 clapping hands: medium-dark skin tone +1F44F 1F3FF ; fully-qualified # 👏🏿 E1.0 clapping hands: dark skin tone +1F64C ; fully-qualified # 🙌 E0.6 raising hands +1F64C 1F3FB ; fully-qualified # 🙌🏻 E1.0 raising hands: light skin tone +1F64C 1F3FC ; fully-qualified # 🙌🏼 E1.0 raising hands: medium-light skin tone +1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone +1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone +1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone +1F450 ; fully-qualified # 👐 E0.6 open hands +1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone +1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone +1F450 1F3FD ; fully-qualified # 👐🏽 E1.0 open hands: medium skin tone +1F450 1F3FE ; fully-qualified # 👐🏾 E1.0 open hands: medium-dark skin tone +1F450 1F3FF ; fully-qualified # 👐🏿 E1.0 open hands: dark skin tone +1F932 ; fully-qualified # 🤲 E5.0 palms up together +1F932 1F3FB ; fully-qualified # 🤲🏻 E5.0 palms up together: light skin tone +1F932 1F3FC ; fully-qualified # 🤲🏼 E5.0 palms up together: medium-light skin tone +1F932 1F3FD ; fully-qualified # 🤲🏽 E5.0 palms up together: medium skin tone +1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone +1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone +1F91D ; fully-qualified # 🤝 E3.0 handshake +1F64F ; fully-qualified # 🙏 E0.6 folded hands +1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone +1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone +1F64F 1F3FD ; fully-qualified # 🙏🏽 E1.0 folded hands: medium skin tone +1F64F 1F3FE ; fully-qualified # 🙏🏾 E1.0 folded hands: medium-dark skin tone +1F64F 1F3FF ; fully-qualified # 🙏🏿 E1.0 folded hands: dark skin tone + +# subgroup: hand-prop +270D FE0F ; fully-qualified # ✍️ E0.7 writing hand +270D ; unqualified # ✍ E0.7 writing hand +270D 1F3FB ; fully-qualified # ✍🏻 E1.0 writing hand: light skin tone +270D 1F3FC ; fully-qualified # ✍🏼 E1.0 writing hand: medium-light skin tone +270D 1F3FD ; fully-qualified # ✍🏽 E1.0 writing hand: medium skin tone +270D 1F3FE ; fully-qualified # ✍🏾 E1.0 writing hand: medium-dark skin tone +270D 1F3FF ; fully-qualified # ✍🏿 E1.0 writing hand: dark skin tone +1F485 ; fully-qualified # 💅 E0.6 nail polish +1F485 1F3FB ; fully-qualified # 💅🏻 E1.0 nail polish: light skin tone +1F485 1F3FC ; fully-qualified # 💅🏼 E1.0 nail polish: medium-light skin tone +1F485 1F3FD ; fully-qualified # 💅🏽 E1.0 nail polish: medium skin tone +1F485 1F3FE ; fully-qualified # 💅🏾 E1.0 nail polish: medium-dark skin tone +1F485 1F3FF ; fully-qualified # 💅🏿 E1.0 nail polish: dark skin tone +1F933 ; fully-qualified # 🤳 E3.0 selfie +1F933 1F3FB ; fully-qualified # 🤳🏻 E3.0 selfie: light skin tone +1F933 1F3FC ; fully-qualified # 🤳🏼 E3.0 selfie: medium-light skin tone +1F933 1F3FD ; fully-qualified # 🤳🏽 E3.0 selfie: medium skin tone +1F933 1F3FE ; fully-qualified # 🤳🏾 E3.0 selfie: medium-dark skin tone +1F933 1F3FF ; fully-qualified # 🤳🏿 E3.0 selfie: dark skin tone + +# subgroup: body-parts +1F4AA ; fully-qualified # 💪 E0.6 flexed biceps +1F4AA 1F3FB ; fully-qualified # 💪🏻 E1.0 flexed biceps: light skin tone +1F4AA 1F3FC ; fully-qualified # 💪🏼 E1.0 flexed biceps: medium-light skin tone +1F4AA 1F3FD ; fully-qualified # 💪🏽 E1.0 flexed biceps: medium skin tone +1F4AA 1F3FE ; fully-qualified # 💪🏾 E1.0 flexed biceps: medium-dark skin tone +1F4AA 1F3FF ; fully-qualified # 💪🏿 E1.0 flexed biceps: dark skin tone +1F9BE ; fully-qualified # 🦾 E12.0 mechanical arm +1F9BF ; fully-qualified # 🦿 E12.0 mechanical leg +1F9B5 ; fully-qualified # 🦵 E11.0 leg +1F9B5 1F3FB ; fully-qualified # 🦵🏻 E11.0 leg: light skin tone +1F9B5 1F3FC ; fully-qualified # 🦵🏼 E11.0 leg: medium-light skin tone +1F9B5 1F3FD ; fully-qualified # 🦵🏽 E11.0 leg: medium skin tone +1F9B5 1F3FE ; fully-qualified # 🦵🏾 E11.0 leg: medium-dark skin tone +1F9B5 1F3FF ; fully-qualified # 🦵🏿 E11.0 leg: dark skin tone +1F9B6 ; fully-qualified # 🦶 E11.0 foot +1F9B6 1F3FB ; fully-qualified # 🦶🏻 E11.0 foot: light skin tone +1F9B6 1F3FC ; fully-qualified # 🦶🏼 E11.0 foot: medium-light skin tone +1F9B6 1F3FD ; fully-qualified # 🦶🏽 E11.0 foot: medium skin tone +1F9B6 1F3FE ; fully-qualified # 🦶🏾 E11.0 foot: medium-dark skin tone +1F9B6 1F3FF ; fully-qualified # 🦶🏿 E11.0 foot: dark skin tone +1F442 ; fully-qualified # 👂 E0.6 ear +1F442 1F3FB ; fully-qualified # 👂🏻 E1.0 ear: light skin tone +1F442 1F3FC ; fully-qualified # 👂🏼 E1.0 ear: medium-light skin tone +1F442 1F3FD ; fully-qualified # 👂🏽 E1.0 ear: medium skin tone +1F442 1F3FE ; fully-qualified # 👂🏾 E1.0 ear: medium-dark skin tone +1F442 1F3FF ; fully-qualified # 👂🏿 E1.0 ear: dark skin tone +1F9BB ; fully-qualified # 🦻 E12.0 ear with hearing aid +1F9BB 1F3FB ; fully-qualified # 🦻🏻 E12.0 ear with hearing aid: light skin tone +1F9BB 1F3FC ; fully-qualified # 🦻🏼 E12.0 ear with hearing aid: medium-light skin tone +1F9BB 1F3FD ; fully-qualified # 🦻🏽 E12.0 ear with hearing aid: medium skin tone +1F9BB 1F3FE ; fully-qualified # 🦻🏾 E12.0 ear with hearing aid: medium-dark skin tone +1F9BB 1F3FF ; fully-qualified # 🦻🏿 E12.0 ear with hearing aid: dark skin tone +1F443 ; fully-qualified # 👃 E0.6 nose +1F443 1F3FB ; fully-qualified # 👃🏻 E1.0 nose: light skin tone +1F443 1F3FC ; fully-qualified # 👃🏼 E1.0 nose: medium-light skin tone +1F443 1F3FD ; fully-qualified # 👃🏽 E1.0 nose: medium skin tone +1F443 1F3FE ; fully-qualified # 👃🏾 E1.0 nose: medium-dark skin tone +1F443 1F3FF ; fully-qualified # 👃🏿 E1.0 nose: dark skin tone +1F9E0 ; fully-qualified # 🧠 E5.0 brain +1FAC0 ; fully-qualified # 🫀 E13.0 anatomical heart +1FAC1 ; fully-qualified # 🫁 E13.0 lungs +1F9B7 ; fully-qualified # 🦷 E11.0 tooth +1F9B4 ; fully-qualified # 🦴 E11.0 bone +1F440 ; fully-qualified # 👀 E0.6 eyes +1F441 FE0F ; fully-qualified # 👁️ E0.7 eye +1F441 ; unqualified # 👁 E0.7 eye +1F445 ; fully-qualified # 👅 E0.6 tongue +1F444 ; fully-qualified # 👄 E0.6 mouth + +# subgroup: person +1F476 ; fully-qualified # 👶 E0.6 baby +1F476 1F3FB ; fully-qualified # 👶🏻 E1.0 baby: light skin tone +1F476 1F3FC ; fully-qualified # 👶🏼 E1.0 baby: medium-light skin tone +1F476 1F3FD ; fully-qualified # 👶🏽 E1.0 baby: medium skin tone +1F476 1F3FE ; fully-qualified # 👶🏾 E1.0 baby: medium-dark skin tone +1F476 1F3FF ; fully-qualified # 👶🏿 E1.0 baby: dark skin tone +1F9D2 ; fully-qualified # 🧒 E5.0 child +1F9D2 1F3FB ; fully-qualified # 🧒🏻 E5.0 child: light skin tone +1F9D2 1F3FC ; fully-qualified # 🧒🏼 E5.0 child: medium-light skin tone +1F9D2 1F3FD ; fully-qualified # 🧒🏽 E5.0 child: medium skin tone +1F9D2 1F3FE ; fully-qualified # 🧒🏾 E5.0 child: medium-dark skin tone +1F9D2 1F3FF ; fully-qualified # 🧒🏿 E5.0 child: dark skin tone +1F466 ; fully-qualified # 👦 E0.6 boy +1F466 1F3FB ; fully-qualified # 👦🏻 E1.0 boy: light skin tone +1F466 1F3FC ; fully-qualified # 👦🏼 E1.0 boy: medium-light skin tone +1F466 1F3FD ; fully-qualified # 👦🏽 E1.0 boy: medium skin tone +1F466 1F3FE ; fully-qualified # 👦🏾 E1.0 boy: medium-dark skin tone +1F466 1F3FF ; fully-qualified # 👦🏿 E1.0 boy: dark skin tone +1F467 ; fully-qualified # 👧 E0.6 girl +1F467 1F3FB ; fully-qualified # 👧🏻 E1.0 girl: light skin tone +1F467 1F3FC ; fully-qualified # 👧🏼 E1.0 girl: medium-light skin tone +1F467 1F3FD ; fully-qualified # 👧🏽 E1.0 girl: medium skin tone +1F467 1F3FE ; fully-qualified # 👧🏾 E1.0 girl: medium-dark skin tone +1F467 1F3FF ; fully-qualified # 👧🏿 E1.0 girl: dark skin tone +1F9D1 ; fully-qualified # 🧑 E5.0 person +1F9D1 1F3FB ; fully-qualified # 🧑🏻 E5.0 person: light skin tone +1F9D1 1F3FC ; fully-qualified # 🧑🏼 E5.0 person: medium-light skin tone +1F9D1 1F3FD ; fully-qualified # 🧑🏽 E5.0 person: medium skin tone +1F9D1 1F3FE ; fully-qualified # 🧑🏾 E5.0 person: medium-dark skin tone +1F9D1 1F3FF ; fully-qualified # 🧑🏿 E5.0 person: dark skin tone +1F471 ; fully-qualified # 👱 E0.6 person: blond hair +1F471 1F3FB ; fully-qualified # 👱🏻 E1.0 person: light skin tone, blond hair +1F471 1F3FC ; fully-qualified # 👱🏼 E1.0 person: medium-light skin tone, blond hair +1F471 1F3FD ; fully-qualified # 👱🏽 E1.0 person: medium skin tone, blond hair +1F471 1F3FE ; fully-qualified # 👱🏾 E1.0 person: medium-dark skin tone, blond hair +1F471 1F3FF ; fully-qualified # 👱🏿 E1.0 person: dark skin tone, blond hair +1F468 ; fully-qualified # 👨 E0.6 man +1F468 1F3FB ; fully-qualified # 👨🏻 E1.0 man: light skin tone +1F468 1F3FC ; fully-qualified # 👨🏼 E1.0 man: medium-light skin tone +1F468 1F3FD ; fully-qualified # 👨🏽 E1.0 man: medium skin tone +1F468 1F3FE ; fully-qualified # 👨🏾 E1.0 man: medium-dark skin tone +1F468 1F3FF ; fully-qualified # 👨🏿 E1.0 man: dark skin tone +1F9D4 ; fully-qualified # 🧔 E5.0 man: beard +1F9D4 1F3FB ; fully-qualified # 🧔🏻 E5.0 man: light skin tone, beard +1F9D4 1F3FC ; fully-qualified # 🧔🏼 E5.0 man: medium-light skin tone, beard +1F9D4 1F3FD ; fully-qualified # 🧔🏽 E5.0 man: medium skin tone, beard +1F9D4 1F3FE ; fully-qualified # 🧔🏾 E5.0 man: medium-dark skin tone, beard +1F9D4 1F3FF ; fully-qualified # 🧔🏿 E5.0 man: dark skin tone, beard +1F468 200D 1F9B0 ; fully-qualified # 👨🦰 E11.0 man: red hair +1F468 1F3FB 200D 1F9B0 ; fully-qualified # 👨🏻🦰 E11.0 man: light skin tone, red hair +1F468 1F3FC 200D 1F9B0 ; fully-qualified # 👨🏼🦰 E11.0 man: medium-light skin tone, red hair +1F468 1F3FD 200D 1F9B0 ; fully-qualified # 👨🏽🦰 E11.0 man: medium skin tone, red hair +1F468 1F3FE 200D 1F9B0 ; fully-qualified # 👨🏾🦰 E11.0 man: medium-dark skin tone, red hair +1F468 1F3FF 200D 1F9B0 ; fully-qualified # 👨🏿🦰 E11.0 man: dark skin tone, red hair +1F468 200D 1F9B1 ; fully-qualified # 👨🦱 E11.0 man: curly hair +1F468 1F3FB 200D 1F9B1 ; fully-qualified # 👨🏻🦱 E11.0 man: light skin tone, curly hair +1F468 1F3FC 200D 1F9B1 ; fully-qualified # 👨🏼🦱 E11.0 man: medium-light skin tone, curly hair +1F468 1F3FD 200D 1F9B1 ; fully-qualified # 👨🏽🦱 E11.0 man: medium skin tone, curly hair +1F468 1F3FE 200D 1F9B1 ; fully-qualified # 👨🏾🦱 E11.0 man: medium-dark skin tone, curly hair +1F468 1F3FF 200D 1F9B1 ; fully-qualified # 👨🏿🦱 E11.0 man: dark skin tone, curly hair +1F468 200D 1F9B3 ; fully-qualified # 👨🦳 E11.0 man: white hair +1F468 1F3FB 200D 1F9B3 ; fully-qualified # 👨🏻🦳 E11.0 man: light skin tone, white hair +1F468 1F3FC 200D 1F9B3 ; fully-qualified # 👨🏼🦳 E11.0 man: medium-light skin tone, white hair +1F468 1F3FD 200D 1F9B3 ; fully-qualified # 👨🏽🦳 E11.0 man: medium skin tone, white hair +1F468 1F3FE 200D 1F9B3 ; fully-qualified # 👨🏾🦳 E11.0 man: medium-dark skin tone, white hair +1F468 1F3FF 200D 1F9B3 ; fully-qualified # 👨🏿🦳 E11.0 man: dark skin tone, white hair +1F468 200D 1F9B2 ; fully-qualified # 👨🦲 E11.0 man: bald +1F468 1F3FB 200D 1F9B2 ; fully-qualified # 👨🏻🦲 E11.0 man: light skin tone, bald +1F468 1F3FC 200D 1F9B2 ; fully-qualified # 👨🏼🦲 E11.0 man: medium-light skin tone, bald +1F468 1F3FD 200D 1F9B2 ; fully-qualified # 👨🏽🦲 E11.0 man: medium skin tone, bald +1F468 1F3FE 200D 1F9B2 ; fully-qualified # 👨🏾🦲 E11.0 man: medium-dark skin tone, bald +1F468 1F3FF 200D 1F9B2 ; fully-qualified # 👨🏿🦲 E11.0 man: dark skin tone, bald +1F469 ; fully-qualified # 👩 E0.6 woman +1F469 1F3FB ; fully-qualified # 👩🏻 E1.0 woman: light skin tone +1F469 1F3FC ; fully-qualified # 👩🏼 E1.0 woman: medium-light skin tone +1F469 1F3FD ; fully-qualified # 👩🏽 E1.0 woman: medium skin tone +1F469 1F3FE ; fully-qualified # 👩🏾 E1.0 woman: medium-dark skin tone +1F469 1F3FF ; fully-qualified # 👩🏿 E1.0 woman: dark skin tone +1F469 200D 1F9B0 ; fully-qualified # 👩🦰 E11.0 woman: red hair +1F469 1F3FB 200D 1F9B0 ; fully-qualified # 👩🏻🦰 E11.0 woman: light skin tone, red hair +1F469 1F3FC 200D 1F9B0 ; fully-qualified # 👩🏼🦰 E11.0 woman: medium-light skin tone, red hair +1F469 1F3FD 200D 1F9B0 ; fully-qualified # 👩🏽🦰 E11.0 woman: medium skin tone, red hair +1F469 1F3FE 200D 1F9B0 ; fully-qualified # 👩🏾🦰 E11.0 woman: medium-dark skin tone, red hair +1F469 1F3FF 200D 1F9B0 ; fully-qualified # 👩🏿🦰 E11.0 woman: dark skin tone, red hair +1F9D1 200D 1F9B0 ; fully-qualified # 🧑🦰 E12.1 person: red hair +1F9D1 1F3FB 200D 1F9B0 ; fully-qualified # 🧑🏻🦰 E12.1 person: light skin tone, red hair +1F9D1 1F3FC 200D 1F9B0 ; fully-qualified # 🧑🏼🦰 E12.1 person: medium-light skin tone, red hair +1F9D1 1F3FD 200D 1F9B0 ; fully-qualified # 🧑🏽🦰 E12.1 person: medium skin tone, red hair +1F9D1 1F3FE 200D 1F9B0 ; fully-qualified # 🧑🏾🦰 E12.1 person: medium-dark skin tone, red hair +1F9D1 1F3FF 200D 1F9B0 ; fully-qualified # 🧑🏿🦰 E12.1 person: dark skin tone, red hair +1F469 200D 1F9B1 ; fully-qualified # 👩🦱 E11.0 woman: curly hair +1F469 1F3FB 200D 1F9B1 ; fully-qualified # 👩🏻🦱 E11.0 woman: light skin tone, curly hair +1F469 1F3FC 200D 1F9B1 ; fully-qualified # 👩🏼🦱 E11.0 woman: medium-light skin tone, curly hair +1F469 1F3FD 200D 1F9B1 ; fully-qualified # 👩🏽🦱 E11.0 woman: medium skin tone, curly hair +1F469 1F3FE 200D 1F9B1 ; fully-qualified # 👩🏾🦱 E11.0 woman: medium-dark skin tone, curly hair +1F469 1F3FF 200D 1F9B1 ; fully-qualified # 👩🏿🦱 E11.0 woman: dark skin tone, curly hair +1F9D1 200D 1F9B1 ; fully-qualified # 🧑🦱 E12.1 person: curly hair +1F9D1 1F3FB 200D 1F9B1 ; fully-qualified # 🧑🏻🦱 E12.1 person: light skin tone, curly hair +1F9D1 1F3FC 200D 1F9B1 ; fully-qualified # 🧑🏼🦱 E12.1 person: medium-light skin tone, curly hair +1F9D1 1F3FD 200D 1F9B1 ; fully-qualified # 🧑🏽🦱 E12.1 person: medium skin tone, curly hair +1F9D1 1F3FE 200D 1F9B1 ; fully-qualified # 🧑🏾🦱 E12.1 person: medium-dark skin tone, curly hair +1F9D1 1F3FF 200D 1F9B1 ; fully-qualified # 🧑🏿🦱 E12.1 person: dark skin tone, curly hair +1F469 200D 1F9B3 ; fully-qualified # 👩🦳 E11.0 woman: white hair +1F469 1F3FB 200D 1F9B3 ; fully-qualified # 👩🏻🦳 E11.0 woman: light skin tone, white hair +1F469 1F3FC 200D 1F9B3 ; fully-qualified # 👩🏼🦳 E11.0 woman: medium-light skin tone, white hair +1F469 1F3FD 200D 1F9B3 ; fully-qualified # 👩🏽🦳 E11.0 woman: medium skin tone, white hair +1F469 1F3FE 200D 1F9B3 ; fully-qualified # 👩🏾🦳 E11.0 woman: medium-dark skin tone, white hair +1F469 1F3FF 200D 1F9B3 ; fully-qualified # 👩🏿🦳 E11.0 woman: dark skin tone, white hair +1F9D1 200D 1F9B3 ; fully-qualified # 🧑🦳 E12.1 person: white hair +1F9D1 1F3FB 200D 1F9B3 ; fully-qualified # 🧑🏻🦳 E12.1 person: light skin tone, white hair +1F9D1 1F3FC 200D 1F9B3 ; fully-qualified # 🧑🏼🦳 E12.1 person: medium-light skin tone, white hair +1F9D1 1F3FD 200D 1F9B3 ; fully-qualified # 🧑🏽🦳 E12.1 person: medium skin tone, white hair +1F9D1 1F3FE 200D 1F9B3 ; fully-qualified # 🧑🏾🦳 E12.1 person: medium-dark skin tone, white hair +1F9D1 1F3FF 200D 1F9B3 ; fully-qualified # 🧑🏿🦳 E12.1 person: dark skin tone, white hair +1F469 200D 1F9B2 ; fully-qualified # 👩🦲 E11.0 woman: bald +1F469 1F3FB 200D 1F9B2 ; fully-qualified # 👩🏻🦲 E11.0 woman: light skin tone, bald +1F469 1F3FC 200D 1F9B2 ; fully-qualified # 👩🏼🦲 E11.0 woman: medium-light skin tone, bald +1F469 1F3FD 200D 1F9B2 ; fully-qualified # 👩🏽🦲 E11.0 woman: medium skin tone, bald +1F469 1F3FE 200D 1F9B2 ; fully-qualified # 👩🏾🦲 E11.0 woman: medium-dark skin tone, bald +1F469 1F3FF 200D 1F9B2 ; fully-qualified # 👩🏿🦲 E11.0 woman: dark skin tone, bald +1F9D1 200D 1F9B2 ; fully-qualified # 🧑🦲 E12.1 person: bald +1F9D1 1F3FB 200D 1F9B2 ; fully-qualified # 🧑🏻🦲 E12.1 person: light skin tone, bald +1F9D1 1F3FC 200D 1F9B2 ; fully-qualified # 🧑🏼🦲 E12.1 person: medium-light skin tone, bald +1F9D1 1F3FD 200D 1F9B2 ; fully-qualified # 🧑🏽🦲 E12.1 person: medium skin tone, bald +1F9D1 1F3FE 200D 1F9B2 ; fully-qualified # 🧑🏾🦲 E12.1 person: medium-dark skin tone, bald +1F9D1 1F3FF 200D 1F9B2 ; fully-qualified # 🧑🏿🦲 E12.1 person: dark skin tone, bald +1F471 200D 2640 FE0F ; fully-qualified # 👱♀️ E4.0 woman: blond hair +1F471 200D 2640 ; minimally-qualified # 👱♀ E4.0 woman: blond hair +1F471 1F3FB 200D 2640 FE0F ; fully-qualified # 👱🏻♀️ E4.0 woman: light skin tone, blond hair +1F471 1F3FB 200D 2640 ; minimally-qualified # 👱🏻♀ E4.0 woman: light skin tone, blond hair +1F471 1F3FC 200D 2640 FE0F ; fully-qualified # 👱🏼♀️ E4.0 woman: medium-light skin tone, blond hair +1F471 1F3FC 200D 2640 ; minimally-qualified # 👱🏼♀ E4.0 woman: medium-light skin tone, blond hair +1F471 1F3FD 200D 2640 FE0F ; fully-qualified # 👱🏽♀️ E4.0 woman: medium skin tone, blond hair +1F471 1F3FD 200D 2640 ; minimally-qualified # 👱🏽♀ E4.0 woman: medium skin tone, blond hair +1F471 1F3FE 200D 2640 FE0F ; fully-qualified # 👱🏾♀️ E4.0 woman: medium-dark skin tone, blond hair +1F471 1F3FE 200D 2640 ; minimally-qualified # 👱🏾♀ E4.0 woman: medium-dark skin tone, blond hair +1F471 1F3FF 200D 2640 FE0F ; fully-qualified # 👱🏿♀️ E4.0 woman: dark skin tone, blond hair +1F471 1F3FF 200D 2640 ; minimally-qualified # 👱🏿♀ E4.0 woman: dark skin tone, blond hair +1F471 200D 2642 FE0F ; fully-qualified # 👱♂️ E4.0 man: blond hair +1F471 200D 2642 ; minimally-qualified # 👱♂ E4.0 man: blond hair +1F471 1F3FB 200D 2642 FE0F ; fully-qualified # 👱🏻♂️ E4.0 man: light skin tone, blond hair +1F471 1F3FB 200D 2642 ; minimally-qualified # 👱🏻♂ E4.0 man: light skin tone, blond hair +1F471 1F3FC 200D 2642 FE0F ; fully-qualified # 👱🏼♂️ E4.0 man: medium-light skin tone, blond hair +1F471 1F3FC 200D 2642 ; minimally-qualified # 👱🏼♂ E4.0 man: medium-light skin tone, blond hair +1F471 1F3FD 200D 2642 FE0F ; fully-qualified # 👱🏽♂️ E4.0 man: medium skin tone, blond hair +1F471 1F3FD 200D 2642 ; minimally-qualified # 👱🏽♂ E4.0 man: medium skin tone, blond hair +1F471 1F3FE 200D 2642 FE0F ; fully-qualified # 👱🏾♂️ E4.0 man: medium-dark skin tone, blond hair +1F471 1F3FE 200D 2642 ; minimally-qualified # 👱🏾♂ E4.0 man: medium-dark skin tone, blond hair +1F471 1F3FF 200D 2642 FE0F ; fully-qualified # 👱🏿♂️ E4.0 man: dark skin tone, blond hair +1F471 1F3FF 200D 2642 ; minimally-qualified # 👱🏿♂ E4.0 man: dark skin tone, blond hair +1F9D3 ; fully-qualified # 🧓 E5.0 older person +1F9D3 1F3FB ; fully-qualified # 🧓🏻 E5.0 older person: light skin tone +1F9D3 1F3FC ; fully-qualified # 🧓🏼 E5.0 older person: medium-light skin tone +1F9D3 1F3FD ; fully-qualified # 🧓🏽 E5.0 older person: medium skin tone +1F9D3 1F3FE ; fully-qualified # 🧓🏾 E5.0 older person: medium-dark skin tone +1F9D3 1F3FF ; fully-qualified # 🧓🏿 E5.0 older person: dark skin tone +1F474 ; fully-qualified # 👴 E0.6 old man +1F474 1F3FB ; fully-qualified # 👴🏻 E1.0 old man: light skin tone +1F474 1F3FC ; fully-qualified # 👴🏼 E1.0 old man: medium-light skin tone +1F474 1F3FD ; fully-qualified # 👴🏽 E1.0 old man: medium skin tone +1F474 1F3FE ; fully-qualified # 👴🏾 E1.0 old man: medium-dark skin tone +1F474 1F3FF ; fully-qualified # 👴🏿 E1.0 old man: dark skin tone +1F475 ; fully-qualified # 👵 E0.6 old woman +1F475 1F3FB ; fully-qualified # 👵🏻 E1.0 old woman: light skin tone +1F475 1F3FC ; fully-qualified # 👵🏼 E1.0 old woman: medium-light skin tone +1F475 1F3FD ; fully-qualified # 👵🏽 E1.0 old woman: medium skin tone +1F475 1F3FE ; fully-qualified # 👵🏾 E1.0 old woman: medium-dark skin tone +1F475 1F3FF ; fully-qualified # 👵🏿 E1.0 old woman: dark skin tone + +# subgroup: person-gesture +1F64D ; fully-qualified # 🙍 E0.6 person frowning +1F64D 1F3FB ; fully-qualified # 🙍🏻 E1.0 person frowning: light skin tone +1F64D 1F3FC ; fully-qualified # 🙍🏼 E1.0 person frowning: medium-light skin tone +1F64D 1F3FD ; fully-qualified # 🙍🏽 E1.0 person frowning: medium skin tone +1F64D 1F3FE ; fully-qualified # 🙍🏾 E1.0 person frowning: medium-dark skin tone +1F64D 1F3FF ; fully-qualified # 🙍🏿 E1.0 person frowning: dark skin tone +1F64D 200D 2642 FE0F ; fully-qualified # 🙍♂️ E4.0 man frowning +1F64D 200D 2642 ; minimally-qualified # 🙍♂ E4.0 man frowning +1F64D 1F3FB 200D 2642 FE0F ; fully-qualified # 🙍🏻♂️ E4.0 man frowning: light skin tone +1F64D 1F3FB 200D 2642 ; minimally-qualified # 🙍🏻♂ E4.0 man frowning: light skin tone +1F64D 1F3FC 200D 2642 FE0F ; fully-qualified # 🙍🏼♂️ E4.0 man frowning: medium-light skin tone +1F64D 1F3FC 200D 2642 ; minimally-qualified # 🙍🏼♂ E4.0 man frowning: medium-light skin tone +1F64D 1F3FD 200D 2642 FE0F ; fully-qualified # 🙍🏽♂️ E4.0 man frowning: medium skin tone +1F64D 1F3FD 200D 2642 ; minimally-qualified # 🙍🏽♂ E4.0 man frowning: medium skin tone +1F64D 1F3FE 200D 2642 FE0F ; fully-qualified # 🙍🏾♂️ E4.0 man frowning: medium-dark skin tone +1F64D 1F3FE 200D 2642 ; minimally-qualified # 🙍🏾♂ E4.0 man frowning: medium-dark skin tone +1F64D 1F3FF 200D 2642 FE0F ; fully-qualified # 🙍🏿♂️ E4.0 man frowning: dark skin tone +1F64D 1F3FF 200D 2642 ; minimally-qualified # 🙍🏿♂ E4.0 man frowning: dark skin tone +1F64D 200D 2640 FE0F ; fully-qualified # 🙍♀️ E4.0 woman frowning +1F64D 200D 2640 ; minimally-qualified # 🙍♀ E4.0 woman frowning +1F64D 1F3FB 200D 2640 FE0F ; fully-qualified # 🙍🏻♀️ E4.0 woman frowning: light skin tone +1F64D 1F3FB 200D 2640 ; minimally-qualified # 🙍🏻♀ E4.0 woman frowning: light skin tone +1F64D 1F3FC 200D 2640 FE0F ; fully-qualified # 🙍🏼♀️ E4.0 woman frowning: medium-light skin tone +1F64D 1F3FC 200D 2640 ; minimally-qualified # 🙍🏼♀ E4.0 woman frowning: medium-light skin tone +1F64D 1F3FD 200D 2640 FE0F ; fully-qualified # 🙍🏽♀️ E4.0 woman frowning: medium skin tone +1F64D 1F3FD 200D 2640 ; minimally-qualified # 🙍🏽♀ E4.0 woman frowning: medium skin tone +1F64D 1F3FE 200D 2640 FE0F ; fully-qualified # 🙍🏾♀️ E4.0 woman frowning: medium-dark skin tone +1F64D 1F3FE 200D 2640 ; minimally-qualified # 🙍🏾♀ E4.0 woman frowning: medium-dark skin tone +1F64D 1F3FF 200D 2640 FE0F ; fully-qualified # 🙍🏿♀️ E4.0 woman frowning: dark skin tone +1F64D 1F3FF 200D 2640 ; minimally-qualified # 🙍🏿♀ E4.0 woman frowning: dark skin tone +1F64E ; fully-qualified # 🙎 E0.6 person pouting +1F64E 1F3FB ; fully-qualified # 🙎🏻 E1.0 person pouting: light skin tone +1F64E 1F3FC ; fully-qualified # 🙎🏼 E1.0 person pouting: medium-light skin tone +1F64E 1F3FD ; fully-qualified # 🙎🏽 E1.0 person pouting: medium skin tone +1F64E 1F3FE ; fully-qualified # 🙎🏾 E1.0 person pouting: medium-dark skin tone +1F64E 1F3FF ; fully-qualified # 🙎🏿 E1.0 person pouting: dark skin tone +1F64E 200D 2642 FE0F ; fully-qualified # 🙎♂️ E4.0 man pouting +1F64E 200D 2642 ; minimally-qualified # 🙎♂ E4.0 man pouting +1F64E 1F3FB 200D 2642 FE0F ; fully-qualified # 🙎🏻♂️ E4.0 man pouting: light skin tone +1F64E 1F3FB 200D 2642 ; minimally-qualified # 🙎🏻♂ E4.0 man pouting: light skin tone +1F64E 1F3FC 200D 2642 FE0F ; fully-qualified # 🙎🏼♂️ E4.0 man pouting: medium-light skin tone +1F64E 1F3FC 200D 2642 ; minimally-qualified # 🙎🏼♂ E4.0 man pouting: medium-light skin tone +1F64E 1F3FD 200D 2642 FE0F ; fully-qualified # 🙎🏽♂️ E4.0 man pouting: medium skin tone +1F64E 1F3FD 200D 2642 ; minimally-qualified # 🙎🏽♂ E4.0 man pouting: medium skin tone +1F64E 1F3FE 200D 2642 FE0F ; fully-qualified # 🙎🏾♂️ E4.0 man pouting: medium-dark skin tone +1F64E 1F3FE 200D 2642 ; minimally-qualified # 🙎🏾♂ E4.0 man pouting: medium-dark skin tone +1F64E 1F3FF 200D 2642 FE0F ; fully-qualified # 🙎🏿♂️ E4.0 man pouting: dark skin tone +1F64E 1F3FF 200D 2642 ; minimally-qualified # 🙎🏿♂ E4.0 man pouting: dark skin tone +1F64E 200D 2640 FE0F ; fully-qualified # 🙎♀️ E4.0 woman pouting +1F64E 200D 2640 ; minimally-qualified # 🙎♀ E4.0 woman pouting +1F64E 1F3FB 200D 2640 FE0F ; fully-qualified # 🙎🏻♀️ E4.0 woman pouting: light skin tone +1F64E 1F3FB 200D 2640 ; minimally-qualified # 🙎🏻♀ E4.0 woman pouting: light skin tone +1F64E 1F3FC 200D 2640 FE0F ; fully-qualified # 🙎🏼♀️ E4.0 woman pouting: medium-light skin tone +1F64E 1F3FC 200D 2640 ; minimally-qualified # 🙎🏼♀ E4.0 woman pouting: medium-light skin tone +1F64E 1F3FD 200D 2640 FE0F ; fully-qualified # 🙎🏽♀️ E4.0 woman pouting: medium skin tone +1F64E 1F3FD 200D 2640 ; minimally-qualified # 🙎🏽♀ E4.0 woman pouting: medium skin tone +1F64E 1F3FE 200D 2640 FE0F ; fully-qualified # 🙎🏾♀️ E4.0 woman pouting: medium-dark skin tone +1F64E 1F3FE 200D 2640 ; minimally-qualified # 🙎🏾♀ E4.0 woman pouting: medium-dark skin tone +1F64E 1F3FF 200D 2640 FE0F ; fully-qualified # 🙎🏿♀️ E4.0 woman pouting: dark skin tone +1F64E 1F3FF 200D 2640 ; minimally-qualified # 🙎🏿♀ E4.0 woman pouting: dark skin tone +1F645 ; fully-qualified # 🙅 E0.6 person gesturing NO +1F645 1F3FB ; fully-qualified # 🙅🏻 E1.0 person gesturing NO: light skin tone +1F645 1F3FC ; fully-qualified # 🙅🏼 E1.0 person gesturing NO: medium-light skin tone +1F645 1F3FD ; fully-qualified # 🙅🏽 E1.0 person gesturing NO: medium skin tone +1F645 1F3FE ; fully-qualified # 🙅🏾 E1.0 person gesturing NO: medium-dark skin tone +1F645 1F3FF ; fully-qualified # 🙅🏿 E1.0 person gesturing NO: dark skin tone +1F645 200D 2642 FE0F ; fully-qualified # 🙅♂️ E4.0 man gesturing NO +1F645 200D 2642 ; minimally-qualified # 🙅♂ E4.0 man gesturing NO +1F645 1F3FB 200D 2642 FE0F ; fully-qualified # 🙅🏻♂️ E4.0 man gesturing NO: light skin tone +1F645 1F3FB 200D 2642 ; minimally-qualified # 🙅🏻♂ E4.0 man gesturing NO: light skin tone +1F645 1F3FC 200D 2642 FE0F ; fully-qualified # 🙅🏼♂️ E4.0 man gesturing NO: medium-light skin tone +1F645 1F3FC 200D 2642 ; minimally-qualified # 🙅🏼♂ E4.0 man gesturing NO: medium-light skin tone +1F645 1F3FD 200D 2642 FE0F ; fully-qualified # 🙅🏽♂️ E4.0 man gesturing NO: medium skin tone +1F645 1F3FD 200D 2642 ; minimally-qualified # 🙅🏽♂ E4.0 man gesturing NO: medium skin tone +1F645 1F3FE 200D 2642 FE0F ; fully-qualified # 🙅🏾♂️ E4.0 man gesturing NO: medium-dark skin tone +1F645 1F3FE 200D 2642 ; minimally-qualified # 🙅🏾♂ E4.0 man gesturing NO: medium-dark skin tone +1F645 1F3FF 200D 2642 FE0F ; fully-qualified # 🙅🏿♂️ E4.0 man gesturing NO: dark skin tone +1F645 1F3FF 200D 2642 ; minimally-qualified # 🙅🏿♂ E4.0 man gesturing NO: dark skin tone +1F645 200D 2640 FE0F ; fully-qualified # 🙅♀️ E4.0 woman gesturing NO +1F645 200D 2640 ; minimally-qualified # 🙅♀ E4.0 woman gesturing NO +1F645 1F3FB 200D 2640 FE0F ; fully-qualified # 🙅🏻♀️ E4.0 woman gesturing NO: light skin tone +1F645 1F3FB 200D 2640 ; minimally-qualified # 🙅🏻♀ E4.0 woman gesturing NO: light skin tone +1F645 1F3FC 200D 2640 FE0F ; fully-qualified # 🙅🏼♀️ E4.0 woman gesturing NO: medium-light skin tone +1F645 1F3FC 200D 2640 ; minimally-qualified # 🙅🏼♀ E4.0 woman gesturing NO: medium-light skin tone +1F645 1F3FD 200D 2640 FE0F ; fully-qualified # 🙅🏽♀️ E4.0 woman gesturing NO: medium skin tone +1F645 1F3FD 200D 2640 ; minimally-qualified # 🙅🏽♀ E4.0 woman gesturing NO: medium skin tone +1F645 1F3FE 200D 2640 FE0F ; fully-qualified # 🙅🏾♀️ E4.0 woman gesturing NO: medium-dark skin tone +1F645 1F3FE 200D 2640 ; minimally-qualified # 🙅🏾♀ E4.0 woman gesturing NO: medium-dark skin tone +1F645 1F3FF 200D 2640 FE0F ; fully-qualified # 🙅🏿♀️ E4.0 woman gesturing NO: dark skin tone +1F645 1F3FF 200D 2640 ; minimally-qualified # 🙅🏿♀ E4.0 woman gesturing NO: dark skin tone +1F646 ; fully-qualified # 🙆 E0.6 person gesturing OK +1F646 1F3FB ; fully-qualified # 🙆🏻 E1.0 person gesturing OK: light skin tone +1F646 1F3FC ; fully-qualified # 🙆🏼 E1.0 person gesturing OK: medium-light skin tone +1F646 1F3FD ; fully-qualified # 🙆🏽 E1.0 person gesturing OK: medium skin tone +1F646 1F3FE ; fully-qualified # 🙆🏾 E1.0 person gesturing OK: medium-dark skin tone +1F646 1F3FF ; fully-qualified # 🙆🏿 E1.0 person gesturing OK: dark skin tone +1F646 200D 2642 FE0F ; fully-qualified # 🙆♂️ E4.0 man gesturing OK +1F646 200D 2642 ; minimally-qualified # 🙆♂ E4.0 man gesturing OK +1F646 1F3FB 200D 2642 FE0F ; fully-qualified # 🙆🏻♂️ E4.0 man gesturing OK: light skin tone +1F646 1F3FB 200D 2642 ; minimally-qualified # 🙆🏻♂ E4.0 man gesturing OK: light skin tone +1F646 1F3FC 200D 2642 FE0F ; fully-qualified # 🙆🏼♂️ E4.0 man gesturing OK: medium-light skin tone +1F646 1F3FC 200D 2642 ; minimally-qualified # 🙆🏼♂ E4.0 man gesturing OK: medium-light skin tone +1F646 1F3FD 200D 2642 FE0F ; fully-qualified # 🙆🏽♂️ E4.0 man gesturing OK: medium skin tone +1F646 1F3FD 200D 2642 ; minimally-qualified # 🙆🏽♂ E4.0 man gesturing OK: medium skin tone +1F646 1F3FE 200D 2642 FE0F ; fully-qualified # 🙆🏾♂️ E4.0 man gesturing OK: medium-dark skin tone +1F646 1F3FE 200D 2642 ; minimally-qualified # 🙆🏾♂ E4.0 man gesturing OK: medium-dark skin tone +1F646 1F3FF 200D 2642 FE0F ; fully-qualified # 🙆🏿♂️ E4.0 man gesturing OK: dark skin tone +1F646 1F3FF 200D 2642 ; minimally-qualified # 🙆🏿♂ E4.0 man gesturing OK: dark skin tone +1F646 200D 2640 FE0F ; fully-qualified # 🙆♀️ E4.0 woman gesturing OK +1F646 200D 2640 ; minimally-qualified # 🙆♀ E4.0 woman gesturing OK +1F646 1F3FB 200D 2640 FE0F ; fully-qualified # 🙆🏻♀️ E4.0 woman gesturing OK: light skin tone +1F646 1F3FB 200D 2640 ; minimally-qualified # 🙆🏻♀ E4.0 woman gesturing OK: light skin tone +1F646 1F3FC 200D 2640 FE0F ; fully-qualified # 🙆🏼♀️ E4.0 woman gesturing OK: medium-light skin tone +1F646 1F3FC 200D 2640 ; minimally-qualified # 🙆🏼♀ E4.0 woman gesturing OK: medium-light skin tone +1F646 1F3FD 200D 2640 FE0F ; fully-qualified # 🙆🏽♀️ E4.0 woman gesturing OK: medium skin tone +1F646 1F3FD 200D 2640 ; minimally-qualified # 🙆🏽♀ E4.0 woman gesturing OK: medium skin tone +1F646 1F3FE 200D 2640 FE0F ; fully-qualified # 🙆🏾♀️ E4.0 woman gesturing OK: medium-dark skin tone +1F646 1F3FE 200D 2640 ; minimally-qualified # 🙆🏾♀ E4.0 woman gesturing OK: medium-dark skin tone +1F646 1F3FF 200D 2640 FE0F ; fully-qualified # 🙆🏿♀️ E4.0 woman gesturing OK: dark skin tone +1F646 1F3FF 200D 2640 ; minimally-qualified # 🙆🏿♀ E4.0 woman gesturing OK: dark skin tone +1F481 ; fully-qualified # 💁 E0.6 person tipping hand +1F481 1F3FB ; fully-qualified # 💁🏻 E1.0 person tipping hand: light skin tone +1F481 1F3FC ; fully-qualified # 💁🏼 E1.0 person tipping hand: medium-light skin tone +1F481 1F3FD ; fully-qualified # 💁🏽 E1.0 person tipping hand: medium skin tone +1F481 1F3FE ; fully-qualified # 💁🏾 E1.0 person tipping hand: medium-dark skin tone +1F481 1F3FF ; fully-qualified # 💁🏿 E1.0 person tipping hand: dark skin tone +1F481 200D 2642 FE0F ; fully-qualified # 💁♂️ E4.0 man tipping hand +1F481 200D 2642 ; minimally-qualified # 💁♂ E4.0 man tipping hand +1F481 1F3FB 200D 2642 FE0F ; fully-qualified # 💁🏻♂️ E4.0 man tipping hand: light skin tone +1F481 1F3FB 200D 2642 ; minimally-qualified # 💁🏻♂ E4.0 man tipping hand: light skin tone +1F481 1F3FC 200D 2642 FE0F ; fully-qualified # 💁🏼♂️ E4.0 man tipping hand: medium-light skin tone +1F481 1F3FC 200D 2642 ; minimally-qualified # 💁🏼♂ E4.0 man tipping hand: medium-light skin tone +1F481 1F3FD 200D 2642 FE0F ; fully-qualified # 💁🏽♂️ E4.0 man tipping hand: medium skin tone +1F481 1F3FD 200D 2642 ; minimally-qualified # 💁🏽♂ E4.0 man tipping hand: medium skin tone +1F481 1F3FE 200D 2642 FE0F ; fully-qualified # 💁🏾♂️ E4.0 man tipping hand: medium-dark skin tone +1F481 1F3FE 200D 2642 ; minimally-qualified # 💁🏾♂ E4.0 man tipping hand: medium-dark skin tone +1F481 1F3FF 200D 2642 FE0F ; fully-qualified # 💁🏿♂️ E4.0 man tipping hand: dark skin tone +1F481 1F3FF 200D 2642 ; minimally-qualified # 💁🏿♂ E4.0 man tipping hand: dark skin tone +1F481 200D 2640 FE0F ; fully-qualified # 💁♀️ E4.0 woman tipping hand +1F481 200D 2640 ; minimally-qualified # 💁♀ E4.0 woman tipping hand +1F481 1F3FB 200D 2640 FE0F ; fully-qualified # 💁🏻♀️ E4.0 woman tipping hand: light skin tone +1F481 1F3FB 200D 2640 ; minimally-qualified # 💁🏻♀ E4.0 woman tipping hand: light skin tone +1F481 1F3FC 200D 2640 FE0F ; fully-qualified # 💁🏼♀️ E4.0 woman tipping hand: medium-light skin tone +1F481 1F3FC 200D 2640 ; minimally-qualified # 💁🏼♀ E4.0 woman tipping hand: medium-light skin tone +1F481 1F3FD 200D 2640 FE0F ; fully-qualified # 💁🏽♀️ E4.0 woman tipping hand: medium skin tone +1F481 1F3FD 200D 2640 ; minimally-qualified # 💁🏽♀ E4.0 woman tipping hand: medium skin tone +1F481 1F3FE 200D 2640 FE0F ; fully-qualified # 💁🏾♀️ E4.0 woman tipping hand: medium-dark skin tone +1F481 1F3FE 200D 2640 ; minimally-qualified # 💁🏾♀ E4.0 woman tipping hand: medium-dark skin tone +1F481 1F3FF 200D 2640 FE0F ; fully-qualified # 💁🏿♀️ E4.0 woman tipping hand: dark skin tone +1F481 1F3FF 200D 2640 ; minimally-qualified # 💁🏿♀ E4.0 woman tipping hand: dark skin tone +1F64B ; fully-qualified # 🙋 E0.6 person raising hand +1F64B 1F3FB ; fully-qualified # 🙋🏻 E1.0 person raising hand: light skin tone +1F64B 1F3FC ; fully-qualified # 🙋🏼 E1.0 person raising hand: medium-light skin tone +1F64B 1F3FD ; fully-qualified # 🙋🏽 E1.0 person raising hand: medium skin tone +1F64B 1F3FE ; fully-qualified # 🙋🏾 E1.0 person raising hand: medium-dark skin tone +1F64B 1F3FF ; fully-qualified # 🙋🏿 E1.0 person raising hand: dark skin tone +1F64B 200D 2642 FE0F ; fully-qualified # 🙋♂️ E4.0 man raising hand +1F64B 200D 2642 ; minimally-qualified # 🙋♂ E4.0 man raising hand +1F64B 1F3FB 200D 2642 FE0F ; fully-qualified # 🙋🏻♂️ E4.0 man raising hand: light skin tone +1F64B 1F3FB 200D 2642 ; minimally-qualified # 🙋🏻♂ E4.0 man raising hand: light skin tone +1F64B 1F3FC 200D 2642 FE0F ; fully-qualified # 🙋🏼♂️ E4.0 man raising hand: medium-light skin tone +1F64B 1F3FC 200D 2642 ; minimally-qualified # 🙋🏼♂ E4.0 man raising hand: medium-light skin tone +1F64B 1F3FD 200D 2642 FE0F ; fully-qualified # 🙋🏽♂️ E4.0 man raising hand: medium skin tone +1F64B 1F3FD 200D 2642 ; minimally-qualified # 🙋🏽♂ E4.0 man raising hand: medium skin tone +1F64B 1F3FE 200D 2642 FE0F ; fully-qualified # 🙋🏾♂️ E4.0 man raising hand: medium-dark skin tone +1F64B 1F3FE 200D 2642 ; minimally-qualified # 🙋🏾♂ E4.0 man raising hand: medium-dark skin tone +1F64B 1F3FF 200D 2642 FE0F ; fully-qualified # 🙋🏿♂️ E4.0 man raising hand: dark skin tone +1F64B 1F3FF 200D 2642 ; minimally-qualified # 🙋🏿♂ E4.0 man raising hand: dark skin tone +1F64B 200D 2640 FE0F ; fully-qualified # 🙋♀️ E4.0 woman raising hand +1F64B 200D 2640 ; minimally-qualified # 🙋♀ E4.0 woman raising hand +1F64B 1F3FB 200D 2640 FE0F ; fully-qualified # 🙋🏻♀️ E4.0 woman raising hand: light skin tone +1F64B 1F3FB 200D 2640 ; minimally-qualified # 🙋🏻♀ E4.0 woman raising hand: light skin tone +1F64B 1F3FC 200D 2640 FE0F ; fully-qualified # 🙋🏼♀️ E4.0 woman raising hand: medium-light skin tone +1F64B 1F3FC 200D 2640 ; minimally-qualified # 🙋🏼♀ E4.0 woman raising hand: medium-light skin tone +1F64B 1F3FD 200D 2640 FE0F ; fully-qualified # 🙋🏽♀️ E4.0 woman raising hand: medium skin tone +1F64B 1F3FD 200D 2640 ; minimally-qualified # 🙋🏽♀ E4.0 woman raising hand: medium skin tone +1F64B 1F3FE 200D 2640 FE0F ; fully-qualified # 🙋🏾♀️ E4.0 woman raising hand: medium-dark skin tone +1F64B 1F3FE 200D 2640 ; minimally-qualified # 🙋🏾♀ E4.0 woman raising hand: medium-dark skin tone +1F64B 1F3FF 200D 2640 FE0F ; fully-qualified # 🙋🏿♀️ E4.0 woman raising hand: dark skin tone +1F64B 1F3FF 200D 2640 ; minimally-qualified # 🙋🏿♀ E4.0 woman raising hand: dark skin tone +1F9CF ; fully-qualified # 🧏 E12.0 deaf person +1F9CF 1F3FB ; fully-qualified # 🧏🏻 E12.0 deaf person: light skin tone +1F9CF 1F3FC ; fully-qualified # 🧏🏼 E12.0 deaf person: medium-light skin tone +1F9CF 1F3FD ; fully-qualified # 🧏🏽 E12.0 deaf person: medium skin tone +1F9CF 1F3FE ; fully-qualified # 🧏🏾 E12.0 deaf person: medium-dark skin tone +1F9CF 1F3FF ; fully-qualified # 🧏🏿 E12.0 deaf person: dark skin tone +1F9CF 200D 2642 FE0F ; fully-qualified # 🧏♂️ E12.0 deaf man +1F9CF 200D 2642 ; minimally-qualified # 🧏♂ E12.0 deaf man +1F9CF 1F3FB 200D 2642 FE0F ; fully-qualified # 🧏🏻♂️ E12.0 deaf man: light skin tone +1F9CF 1F3FB 200D 2642 ; minimally-qualified # 🧏🏻♂ E12.0 deaf man: light skin tone +1F9CF 1F3FC 200D 2642 FE0F ; fully-qualified # 🧏🏼♂️ E12.0 deaf man: medium-light skin tone +1F9CF 1F3FC 200D 2642 ; minimally-qualified # 🧏🏼♂ E12.0 deaf man: medium-light skin tone +1F9CF 1F3FD 200D 2642 FE0F ; fully-qualified # 🧏🏽♂️ E12.0 deaf man: medium skin tone +1F9CF 1F3FD 200D 2642 ; minimally-qualified # 🧏🏽♂ E12.0 deaf man: medium skin tone +1F9CF 1F3FE 200D 2642 FE0F ; fully-qualified # 🧏🏾♂️ E12.0 deaf man: medium-dark skin tone +1F9CF 1F3FE 200D 2642 ; minimally-qualified # 🧏🏾♂ E12.0 deaf man: medium-dark skin tone +1F9CF 1F3FF 200D 2642 FE0F ; fully-qualified # 🧏🏿♂️ E12.0 deaf man: dark skin tone +1F9CF 1F3FF 200D 2642 ; minimally-qualified # 🧏🏿♂ E12.0 deaf man: dark skin tone +1F9CF 200D 2640 FE0F ; fully-qualified # 🧏♀️ E12.0 deaf woman +1F9CF 200D 2640 ; minimally-qualified # 🧏♀ E12.0 deaf woman +1F9CF 1F3FB 200D 2640 FE0F ; fully-qualified # 🧏🏻♀️ E12.0 deaf woman: light skin tone +1F9CF 1F3FB 200D 2640 ; minimally-qualified # 🧏🏻♀ E12.0 deaf woman: light skin tone +1F9CF 1F3FC 200D 2640 FE0F ; fully-qualified # 🧏🏼♀️ E12.0 deaf woman: medium-light skin tone +1F9CF 1F3FC 200D 2640 ; minimally-qualified # 🧏🏼♀ E12.0 deaf woman: medium-light skin tone +1F9CF 1F3FD 200D 2640 FE0F ; fully-qualified # 🧏🏽♀️ E12.0 deaf woman: medium skin tone +1F9CF 1F3FD 200D 2640 ; minimally-qualified # 🧏🏽♀ E12.0 deaf woman: medium skin tone +1F9CF 1F3FE 200D 2640 FE0F ; fully-qualified # 🧏🏾♀️ E12.0 deaf woman: medium-dark skin tone +1F9CF 1F3FE 200D 2640 ; minimally-qualified # 🧏🏾♀ E12.0 deaf woman: medium-dark skin tone +1F9CF 1F3FF 200D 2640 FE0F ; fully-qualified # 🧏🏿♀️ E12.0 deaf woman: dark skin tone +1F9CF 1F3FF 200D 2640 ; minimally-qualified # 🧏🏿♀ E12.0 deaf woman: dark skin tone +1F647 ; fully-qualified # 🙇 E0.6 person bowing +1F647 1F3FB ; fully-qualified # 🙇🏻 E1.0 person bowing: light skin tone +1F647 1F3FC ; fully-qualified # 🙇🏼 E1.0 person bowing: medium-light skin tone +1F647 1F3FD ; fully-qualified # 🙇🏽 E1.0 person bowing: medium skin tone +1F647 1F3FE ; fully-qualified # 🙇🏾 E1.0 person bowing: medium-dark skin tone +1F647 1F3FF ; fully-qualified # 🙇🏿 E1.0 person bowing: dark skin tone +1F647 200D 2642 FE0F ; fully-qualified # 🙇♂️ E4.0 man bowing +1F647 200D 2642 ; minimally-qualified # 🙇♂ E4.0 man bowing +1F647 1F3FB 200D 2642 FE0F ; fully-qualified # 🙇🏻♂️ E4.0 man bowing: light skin tone +1F647 1F3FB 200D 2642 ; minimally-qualified # 🙇🏻♂ E4.0 man bowing: light skin tone +1F647 1F3FC 200D 2642 FE0F ; fully-qualified # 🙇🏼♂️ E4.0 man bowing: medium-light skin tone +1F647 1F3FC 200D 2642 ; minimally-qualified # 🙇🏼♂ E4.0 man bowing: medium-light skin tone +1F647 1F3FD 200D 2642 FE0F ; fully-qualified # 🙇🏽♂️ E4.0 man bowing: medium skin tone +1F647 1F3FD 200D 2642 ; minimally-qualified # 🙇🏽♂ E4.0 man bowing: medium skin tone +1F647 1F3FE 200D 2642 FE0F ; fully-qualified # 🙇🏾♂️ E4.0 man bowing: medium-dark skin tone +1F647 1F3FE 200D 2642 ; minimally-qualified # 🙇🏾♂ E4.0 man bowing: medium-dark skin tone +1F647 1F3FF 200D 2642 FE0F ; fully-qualified # 🙇🏿♂️ E4.0 man bowing: dark skin tone +1F647 1F3FF 200D 2642 ; minimally-qualified # 🙇🏿♂ E4.0 man bowing: dark skin tone +1F647 200D 2640 FE0F ; fully-qualified # 🙇♀️ E4.0 woman bowing +1F647 200D 2640 ; minimally-qualified # 🙇♀ E4.0 woman bowing +1F647 1F3FB 200D 2640 FE0F ; fully-qualified # 🙇🏻♀️ E4.0 woman bowing: light skin tone +1F647 1F3FB 200D 2640 ; minimally-qualified # 🙇🏻♀ E4.0 woman bowing: light skin tone +1F647 1F3FC 200D 2640 FE0F ; fully-qualified # 🙇🏼♀️ E4.0 woman bowing: medium-light skin tone +1F647 1F3FC 200D 2640 ; minimally-qualified # 🙇🏼♀ E4.0 woman bowing: medium-light skin tone +1F647 1F3FD 200D 2640 FE0F ; fully-qualified # 🙇🏽♀️ E4.0 woman bowing: medium skin tone +1F647 1F3FD 200D 2640 ; minimally-qualified # 🙇🏽♀ E4.0 woman bowing: medium skin tone +1F647 1F3FE 200D 2640 FE0F ; fully-qualified # 🙇🏾♀️ E4.0 woman bowing: medium-dark skin tone +1F647 1F3FE 200D 2640 ; minimally-qualified # 🙇🏾♀ E4.0 woman bowing: medium-dark skin tone +1F647 1F3FF 200D 2640 FE0F ; fully-qualified # 🙇🏿♀️ E4.0 woman bowing: dark skin tone +1F647 1F3FF 200D 2640 ; minimally-qualified # 🙇🏿♀ E4.0 woman bowing: dark skin tone +1F926 ; fully-qualified # 🤦 E3.0 person facepalming +1F926 1F3FB ; fully-qualified # 🤦🏻 E3.0 person facepalming: light skin tone +1F926 1F3FC ; fully-qualified # 🤦🏼 E3.0 person facepalming: medium-light skin tone +1F926 1F3FD ; fully-qualified # 🤦🏽 E3.0 person facepalming: medium skin tone +1F926 1F3FE ; fully-qualified # 🤦🏾 E3.0 person facepalming: medium-dark skin tone +1F926 1F3FF ; fully-qualified # 🤦🏿 E3.0 person facepalming: dark skin tone +1F926 200D 2642 FE0F ; fully-qualified # 🤦♂️ E4.0 man facepalming +1F926 200D 2642 ; minimally-qualified # 🤦♂ E4.0 man facepalming +1F926 1F3FB 200D 2642 FE0F ; fully-qualified # 🤦🏻♂️ E4.0 man facepalming: light skin tone +1F926 1F3FB 200D 2642 ; minimally-qualified # 🤦🏻♂ E4.0 man facepalming: light skin tone +1F926 1F3FC 200D 2642 FE0F ; fully-qualified # 🤦🏼♂️ E4.0 man facepalming: medium-light skin tone +1F926 1F3FC 200D 2642 ; minimally-qualified # 🤦🏼♂ E4.0 man facepalming: medium-light skin tone +1F926 1F3FD 200D 2642 FE0F ; fully-qualified # 🤦🏽♂️ E4.0 man facepalming: medium skin tone +1F926 1F3FD 200D 2642 ; minimally-qualified # 🤦🏽♂ E4.0 man facepalming: medium skin tone +1F926 1F3FE 200D 2642 FE0F ; fully-qualified # 🤦🏾♂️ E4.0 man facepalming: medium-dark skin tone +1F926 1F3FE 200D 2642 ; minimally-qualified # 🤦🏾♂ E4.0 man facepalming: medium-dark skin tone +1F926 1F3FF 200D 2642 FE0F ; fully-qualified # 🤦🏿♂️ E4.0 man facepalming: dark skin tone +1F926 1F3FF 200D 2642 ; minimally-qualified # 🤦🏿♂ E4.0 man facepalming: dark skin tone +1F926 200D 2640 FE0F ; fully-qualified # 🤦♀️ E4.0 woman facepalming +1F926 200D 2640 ; minimally-qualified # 🤦♀ E4.0 woman facepalming +1F926 1F3FB 200D 2640 FE0F ; fully-qualified # 🤦🏻♀️ E4.0 woman facepalming: light skin tone +1F926 1F3FB 200D 2640 ; minimally-qualified # 🤦🏻♀ E4.0 woman facepalming: light skin tone +1F926 1F3FC 200D 2640 FE0F ; fully-qualified # 🤦🏼♀️ E4.0 woman facepalming: medium-light skin tone +1F926 1F3FC 200D 2640 ; minimally-qualified # 🤦🏼♀ E4.0 woman facepalming: medium-light skin tone +1F926 1F3FD 200D 2640 FE0F ; fully-qualified # 🤦🏽♀️ E4.0 woman facepalming: medium skin tone +1F926 1F3FD 200D 2640 ; minimally-qualified # 🤦🏽♀ E4.0 woman facepalming: medium skin tone +1F926 1F3FE 200D 2640 FE0F ; fully-qualified # 🤦🏾♀️ E4.0 woman facepalming: medium-dark skin tone +1F926 1F3FE 200D 2640 ; minimally-qualified # 🤦🏾♀ E4.0 woman facepalming: medium-dark skin tone +1F926 1F3FF 200D 2640 FE0F ; fully-qualified # 🤦🏿♀️ E4.0 woman facepalming: dark skin tone +1F926 1F3FF 200D 2640 ; minimally-qualified # 🤦🏿♀ E4.0 woman facepalming: dark skin tone +1F937 ; fully-qualified # 🤷 E3.0 person shrugging +1F937 1F3FB ; fully-qualified # 🤷🏻 E3.0 person shrugging: light skin tone +1F937 1F3FC ; fully-qualified # 🤷🏼 E3.0 person shrugging: medium-light skin tone +1F937 1F3FD ; fully-qualified # 🤷🏽 E3.0 person shrugging: medium skin tone +1F937 1F3FE ; fully-qualified # 🤷🏾 E3.0 person shrugging: medium-dark skin tone +1F937 1F3FF ; fully-qualified # 🤷🏿 E3.0 person shrugging: dark skin tone +1F937 200D 2642 FE0F ; fully-qualified # 🤷♂️ E4.0 man shrugging +1F937 200D 2642 ; minimally-qualified # 🤷♂ E4.0 man shrugging +1F937 1F3FB 200D 2642 FE0F ; fully-qualified # 🤷🏻♂️ E4.0 man shrugging: light skin tone +1F937 1F3FB 200D 2642 ; minimally-qualified # 🤷🏻♂ E4.0 man shrugging: light skin tone +1F937 1F3FC 200D 2642 FE0F ; fully-qualified # 🤷🏼♂️ E4.0 man shrugging: medium-light skin tone +1F937 1F3FC 200D 2642 ; minimally-qualified # 🤷🏼♂ E4.0 man shrugging: medium-light skin tone +1F937 1F3FD 200D 2642 FE0F ; fully-qualified # 🤷🏽♂️ E4.0 man shrugging: medium skin tone +1F937 1F3FD 200D 2642 ; minimally-qualified # 🤷🏽♂ E4.0 man shrugging: medium skin tone +1F937 1F3FE 200D 2642 FE0F ; fully-qualified # 🤷🏾♂️ E4.0 man shrugging: medium-dark skin tone +1F937 1F3FE 200D 2642 ; minimally-qualified # 🤷🏾♂ E4.0 man shrugging: medium-dark skin tone +1F937 1F3FF 200D 2642 FE0F ; fully-qualified # 🤷🏿♂️ E4.0 man shrugging: dark skin tone +1F937 1F3FF 200D 2642 ; minimally-qualified # 🤷🏿♂ E4.0 man shrugging: dark skin tone +1F937 200D 2640 FE0F ; fully-qualified # 🤷♀️ E4.0 woman shrugging +1F937 200D 2640 ; minimally-qualified # 🤷♀ E4.0 woman shrugging +1F937 1F3FB 200D 2640 FE0F ; fully-qualified # 🤷🏻♀️ E4.0 woman shrugging: light skin tone +1F937 1F3FB 200D 2640 ; minimally-qualified # 🤷🏻♀ E4.0 woman shrugging: light skin tone +1F937 1F3FC 200D 2640 FE0F ; fully-qualified # 🤷🏼♀️ E4.0 woman shrugging: medium-light skin tone +1F937 1F3FC 200D 2640 ; minimally-qualified # 🤷🏼♀ E4.0 woman shrugging: medium-light skin tone +1F937 1F3FD 200D 2640 FE0F ; fully-qualified # 🤷🏽♀️ E4.0 woman shrugging: medium skin tone +1F937 1F3FD 200D 2640 ; minimally-qualified # 🤷🏽♀ E4.0 woman shrugging: medium skin tone +1F937 1F3FE 200D 2640 FE0F ; fully-qualified # 🤷🏾♀️ E4.0 woman shrugging: medium-dark skin tone +1F937 1F3FE 200D 2640 ; minimally-qualified # 🤷🏾♀ E4.0 woman shrugging: medium-dark skin tone +1F937 1F3FF 200D 2640 FE0F ; fully-qualified # 🤷🏿♀️ E4.0 woman shrugging: dark skin tone +1F937 1F3FF 200D 2640 ; minimally-qualified # 🤷🏿♀ E4.0 woman shrugging: dark skin tone + +# subgroup: person-role +1F9D1 200D 2695 FE0F ; fully-qualified # 🧑⚕️ E12.1 health worker +1F9D1 200D 2695 ; minimally-qualified # 🧑⚕ E12.1 health worker +1F9D1 1F3FB 200D 2695 FE0F ; fully-qualified # 🧑🏻⚕️ E12.1 health worker: light skin tone +1F9D1 1F3FB 200D 2695 ; minimally-qualified # 🧑🏻⚕ E12.1 health worker: light skin tone +1F9D1 1F3FC 200D 2695 FE0F ; fully-qualified # 🧑🏼⚕️ E12.1 health worker: medium-light skin tone +1F9D1 1F3FC 200D 2695 ; minimally-qualified # 🧑🏼⚕ E12.1 health worker: medium-light skin tone +1F9D1 1F3FD 200D 2695 FE0F ; fully-qualified # 🧑🏽⚕️ E12.1 health worker: medium skin tone +1F9D1 1F3FD 200D 2695 ; minimally-qualified # 🧑🏽⚕ E12.1 health worker: medium skin tone +1F9D1 1F3FE 200D 2695 FE0F ; fully-qualified # 🧑🏾⚕️ E12.1 health worker: medium-dark skin tone +1F9D1 1F3FE 200D 2695 ; minimally-qualified # 🧑🏾⚕ E12.1 health worker: medium-dark skin tone +1F9D1 1F3FF 200D 2695 FE0F ; fully-qualified # 🧑🏿⚕️ E12.1 health worker: dark skin tone +1F9D1 1F3FF 200D 2695 ; minimally-qualified # 🧑🏿⚕ E12.1 health worker: dark skin tone +1F468 200D 2695 FE0F ; fully-qualified # 👨⚕️ E4.0 man health worker +1F468 200D 2695 ; minimally-qualified # 👨⚕ E4.0 man health worker +1F468 1F3FB 200D 2695 FE0F ; fully-qualified # 👨🏻⚕️ E4.0 man health worker: light skin tone +1F468 1F3FB 200D 2695 ; minimally-qualified # 👨🏻⚕ E4.0 man health worker: light skin tone +1F468 1F3FC 200D 2695 FE0F ; fully-qualified # 👨🏼⚕️ E4.0 man health worker: medium-light skin tone +1F468 1F3FC 200D 2695 ; minimally-qualified # 👨🏼⚕ E4.0 man health worker: medium-light skin tone +1F468 1F3FD 200D 2695 FE0F ; fully-qualified # 👨🏽⚕️ E4.0 man health worker: medium skin tone +1F468 1F3FD 200D 2695 ; minimally-qualified # 👨🏽⚕ E4.0 man health worker: medium skin tone +1F468 1F3FE 200D 2695 FE0F ; fully-qualified # 👨🏾⚕️ E4.0 man health worker: medium-dark skin tone +1F468 1F3FE 200D 2695 ; minimally-qualified # 👨🏾⚕ E4.0 man health worker: medium-dark skin tone +1F468 1F3FF 200D 2695 FE0F ; fully-qualified # 👨🏿⚕️ E4.0 man health worker: dark skin tone +1F468 1F3FF 200D 2695 ; minimally-qualified # 👨🏿⚕ E4.0 man health worker: dark skin tone +1F469 200D 2695 FE0F ; fully-qualified # 👩⚕️ E4.0 woman health worker +1F469 200D 2695 ; minimally-qualified # 👩⚕ E4.0 woman health worker +1F469 1F3FB 200D 2695 FE0F ; fully-qualified # 👩🏻⚕️ E4.0 woman health worker: light skin tone +1F469 1F3FB 200D 2695 ; minimally-qualified # 👩🏻⚕ E4.0 woman health worker: light skin tone +1F469 1F3FC 200D 2695 FE0F ; fully-qualified # 👩🏼⚕️ E4.0 woman health worker: medium-light skin tone +1F469 1F3FC 200D 2695 ; minimally-qualified # 👩🏼⚕ E4.0 woman health worker: medium-light skin tone +1F469 1F3FD 200D 2695 FE0F ; fully-qualified # 👩🏽⚕️ E4.0 woman health worker: medium skin tone +1F469 1F3FD 200D 2695 ; minimally-qualified # 👩🏽⚕ E4.0 woman health worker: medium skin tone +1F469 1F3FE 200D 2695 FE0F ; fully-qualified # 👩🏾⚕️ E4.0 woman health worker: medium-dark skin tone +1F469 1F3FE 200D 2695 ; minimally-qualified # 👩🏾⚕ E4.0 woman health worker: medium-dark skin tone +1F469 1F3FF 200D 2695 FE0F ; fully-qualified # 👩🏿⚕️ E4.0 woman health worker: dark skin tone +1F469 1F3FF 200D 2695 ; minimally-qualified # 👩🏿⚕ E4.0 woman health worker: dark skin tone +1F9D1 200D 1F393 ; fully-qualified # 🧑🎓 E12.1 student +1F9D1 1F3FB 200D 1F393 ; fully-qualified # 🧑🏻🎓 E12.1 student: light skin tone +1F9D1 1F3FC 200D 1F393 ; fully-qualified # 🧑🏼🎓 E12.1 student: medium-light skin tone +1F9D1 1F3FD 200D 1F393 ; fully-qualified # 🧑🏽🎓 E12.1 student: medium skin tone +1F9D1 1F3FE 200D 1F393 ; fully-qualified # 🧑🏾🎓 E12.1 student: medium-dark skin tone +1F9D1 1F3FF 200D 1F393 ; fully-qualified # 🧑🏿🎓 E12.1 student: dark skin tone +1F468 200D 1F393 ; fully-qualified # 👨🎓 E4.0 man student +1F468 1F3FB 200D 1F393 ; fully-qualified # 👨🏻🎓 E4.0 man student: light skin tone +1F468 1F3FC 200D 1F393 ; fully-qualified # 👨🏼🎓 E4.0 man student: medium-light skin tone +1F468 1F3FD 200D 1F393 ; fully-qualified # 👨🏽🎓 E4.0 man student: medium skin tone +1F468 1F3FE 200D 1F393 ; fully-qualified # 👨🏾🎓 E4.0 man student: medium-dark skin tone +1F468 1F3FF 200D 1F393 ; fully-qualified # 👨🏿🎓 E4.0 man student: dark skin tone +1F469 200D 1F393 ; fully-qualified # 👩🎓 E4.0 woman student +1F469 1F3FB 200D 1F393 ; fully-qualified # 👩🏻🎓 E4.0 woman student: light skin tone +1F469 1F3FC 200D 1F393 ; fully-qualified # 👩🏼🎓 E4.0 woman student: medium-light skin tone +1F469 1F3FD 200D 1F393 ; fully-qualified # 👩🏽🎓 E4.0 woman student: medium skin tone +1F469 1F3FE 200D 1F393 ; fully-qualified # 👩🏾🎓 E4.0 woman student: medium-dark skin tone +1F469 1F3FF 200D 1F393 ; fully-qualified # 👩🏿🎓 E4.0 woman student: dark skin tone +1F9D1 200D 1F3EB ; fully-qualified # 🧑🏫 E12.1 teacher +1F9D1 1F3FB 200D 1F3EB ; fully-qualified # 🧑🏻🏫 E12.1 teacher: light skin tone +1F9D1 1F3FC 200D 1F3EB ; fully-qualified # 🧑🏼🏫 E12.1 teacher: medium-light skin tone +1F9D1 1F3FD 200D 1F3EB ; fully-qualified # 🧑🏽🏫 E12.1 teacher: medium skin tone +1F9D1 1F3FE 200D 1F3EB ; fully-qualified # 🧑🏾🏫 E12.1 teacher: medium-dark skin tone +1F9D1 1F3FF 200D 1F3EB ; fully-qualified # 🧑🏿🏫 E12.1 teacher: dark skin tone +1F468 200D 1F3EB ; fully-qualified # 👨🏫 E4.0 man teacher +1F468 1F3FB 200D 1F3EB ; fully-qualified # 👨🏻🏫 E4.0 man teacher: light skin tone +1F468 1F3FC 200D 1F3EB ; fully-qualified # 👨🏼🏫 E4.0 man teacher: medium-light skin tone +1F468 1F3FD 200D 1F3EB ; fully-qualified # 👨🏽🏫 E4.0 man teacher: medium skin tone +1F468 1F3FE 200D 1F3EB ; fully-qualified # 👨🏾🏫 E4.0 man teacher: medium-dark skin tone +1F468 1F3FF 200D 1F3EB ; fully-qualified # 👨🏿🏫 E4.0 man teacher: dark skin tone +1F469 200D 1F3EB ; fully-qualified # 👩🏫 E4.0 woman teacher +1F469 1F3FB 200D 1F3EB ; fully-qualified # 👩🏻🏫 E4.0 woman teacher: light skin tone +1F469 1F3FC 200D 1F3EB ; fully-qualified # 👩🏼🏫 E4.0 woman teacher: medium-light skin tone +1F469 1F3FD 200D 1F3EB ; fully-qualified # 👩🏽🏫 E4.0 woman teacher: medium skin tone +1F469 1F3FE 200D 1F3EB ; fully-qualified # 👩🏾🏫 E4.0 woman teacher: medium-dark skin tone +1F469 1F3FF 200D 1F3EB ; fully-qualified # 👩🏿🏫 E4.0 woman teacher: dark skin tone +1F9D1 200D 2696 FE0F ; fully-qualified # 🧑⚖️ E12.1 judge +1F9D1 200D 2696 ; minimally-qualified # 🧑⚖ E12.1 judge +1F9D1 1F3FB 200D 2696 FE0F ; fully-qualified # 🧑🏻⚖️ E12.1 judge: light skin tone +1F9D1 1F3FB 200D 2696 ; minimally-qualified # 🧑🏻⚖ E12.1 judge: light skin tone +1F9D1 1F3FC 200D 2696 FE0F ; fully-qualified # 🧑🏼⚖️ E12.1 judge: medium-light skin tone +1F9D1 1F3FC 200D 2696 ; minimally-qualified # 🧑🏼⚖ E12.1 judge: medium-light skin tone +1F9D1 1F3FD 200D 2696 FE0F ; fully-qualified # 🧑🏽⚖️ E12.1 judge: medium skin tone +1F9D1 1F3FD 200D 2696 ; minimally-qualified # 🧑🏽⚖ E12.1 judge: medium skin tone +1F9D1 1F3FE 200D 2696 FE0F ; fully-qualified # 🧑🏾⚖️ E12.1 judge: medium-dark skin tone +1F9D1 1F3FE 200D 2696 ; minimally-qualified # 🧑🏾⚖ E12.1 judge: medium-dark skin tone +1F9D1 1F3FF 200D 2696 FE0F ; fully-qualified # 🧑🏿⚖️ E12.1 judge: dark skin tone +1F9D1 1F3FF 200D 2696 ; minimally-qualified # 🧑🏿⚖ E12.1 judge: dark skin tone +1F468 200D 2696 FE0F ; fully-qualified # 👨⚖️ E4.0 man judge +1F468 200D 2696 ; minimally-qualified # 👨⚖ E4.0 man judge +1F468 1F3FB 200D 2696 FE0F ; fully-qualified # 👨🏻⚖️ E4.0 man judge: light skin tone +1F468 1F3FB 200D 2696 ; minimally-qualified # 👨🏻⚖ E4.0 man judge: light skin tone +1F468 1F3FC 200D 2696 FE0F ; fully-qualified # 👨🏼⚖️ E4.0 man judge: medium-light skin tone +1F468 1F3FC 200D 2696 ; minimally-qualified # 👨🏼⚖ E4.0 man judge: medium-light skin tone +1F468 1F3FD 200D 2696 FE0F ; fully-qualified # 👨🏽⚖️ E4.0 man judge: medium skin tone +1F468 1F3FD 200D 2696 ; minimally-qualified # 👨🏽⚖ E4.0 man judge: medium skin tone +1F468 1F3FE 200D 2696 FE0F ; fully-qualified # 👨🏾⚖️ E4.0 man judge: medium-dark skin tone +1F468 1F3FE 200D 2696 ; minimally-qualified # 👨🏾⚖ E4.0 man judge: medium-dark skin tone +1F468 1F3FF 200D 2696 FE0F ; fully-qualified # 👨🏿⚖️ E4.0 man judge: dark skin tone +1F468 1F3FF 200D 2696 ; minimally-qualified # 👨🏿⚖ E4.0 man judge: dark skin tone +1F469 200D 2696 FE0F ; fully-qualified # 👩⚖️ E4.0 woman judge +1F469 200D 2696 ; minimally-qualified # 👩⚖ E4.0 woman judge +1F469 1F3FB 200D 2696 FE0F ; fully-qualified # 👩🏻⚖️ E4.0 woman judge: light skin tone +1F469 1F3FB 200D 2696 ; minimally-qualified # 👩🏻⚖ E4.0 woman judge: light skin tone +1F469 1F3FC 200D 2696 FE0F ; fully-qualified # 👩🏼⚖️ E4.0 woman judge: medium-light skin tone +1F469 1F3FC 200D 2696 ; minimally-qualified # 👩🏼⚖ E4.0 woman judge: medium-light skin tone +1F469 1F3FD 200D 2696 FE0F ; fully-qualified # 👩🏽⚖️ E4.0 woman judge: medium skin tone +1F469 1F3FD 200D 2696 ; minimally-qualified # 👩🏽⚖ E4.0 woman judge: medium skin tone +1F469 1F3FE 200D 2696 FE0F ; fully-qualified # 👩🏾⚖️ E4.0 woman judge: medium-dark skin tone +1F469 1F3FE 200D 2696 ; minimally-qualified # 👩🏾⚖ E4.0 woman judge: medium-dark skin tone +1F469 1F3FF 200D 2696 FE0F ; fully-qualified # 👩🏿⚖️ E4.0 woman judge: dark skin tone +1F469 1F3FF 200D 2696 ; minimally-qualified # 👩🏿⚖ E4.0 woman judge: dark skin tone +1F9D1 200D 1F33E ; fully-qualified # 🧑🌾 E12.1 farmer +1F9D1 1F3FB 200D 1F33E ; fully-qualified # 🧑🏻🌾 E12.1 farmer: light skin tone +1F9D1 1F3FC 200D 1F33E ; fully-qualified # 🧑🏼🌾 E12.1 farmer: medium-light skin tone +1F9D1 1F3FD 200D 1F33E ; fully-qualified # 🧑🏽🌾 E12.1 farmer: medium skin tone +1F9D1 1F3FE 200D 1F33E ; fully-qualified # 🧑🏾🌾 E12.1 farmer: medium-dark skin tone +1F9D1 1F3FF 200D 1F33E ; fully-qualified # 🧑🏿🌾 E12.1 farmer: dark skin tone +1F468 200D 1F33E ; fully-qualified # 👨🌾 E4.0 man farmer +1F468 1F3FB 200D 1F33E ; fully-qualified # 👨🏻🌾 E4.0 man farmer: light skin tone +1F468 1F3FC 200D 1F33E ; fully-qualified # 👨🏼🌾 E4.0 man farmer: medium-light skin tone +1F468 1F3FD 200D 1F33E ; fully-qualified # 👨🏽🌾 E4.0 man farmer: medium skin tone +1F468 1F3FE 200D 1F33E ; fully-qualified # 👨🏾🌾 E4.0 man farmer: medium-dark skin tone +1F468 1F3FF 200D 1F33E ; fully-qualified # 👨🏿🌾 E4.0 man farmer: dark skin tone +1F469 200D 1F33E ; fully-qualified # 👩🌾 E4.0 woman farmer +1F469 1F3FB 200D 1F33E ; fully-qualified # 👩🏻🌾 E4.0 woman farmer: light skin tone +1F469 1F3FC 200D 1F33E ; fully-qualified # 👩🏼🌾 E4.0 woman farmer: medium-light skin tone +1F469 1F3FD 200D 1F33E ; fully-qualified # 👩🏽🌾 E4.0 woman farmer: medium skin tone +1F469 1F3FE 200D 1F33E ; fully-qualified # 👩🏾🌾 E4.0 woman farmer: medium-dark skin tone +1F469 1F3FF 200D 1F33E ; fully-qualified # 👩🏿🌾 E4.0 woman farmer: dark skin tone +1F9D1 200D 1F373 ; fully-qualified # 🧑🍳 E12.1 cook +1F9D1 1F3FB 200D 1F373 ; fully-qualified # 🧑🏻🍳 E12.1 cook: light skin tone +1F9D1 1F3FC 200D 1F373 ; fully-qualified # 🧑🏼🍳 E12.1 cook: medium-light skin tone +1F9D1 1F3FD 200D 1F373 ; fully-qualified # 🧑🏽🍳 E12.1 cook: medium skin tone +1F9D1 1F3FE 200D 1F373 ; fully-qualified # 🧑🏾🍳 E12.1 cook: medium-dark skin tone +1F9D1 1F3FF 200D 1F373 ; fully-qualified # 🧑🏿🍳 E12.1 cook: dark skin tone +1F468 200D 1F373 ; fully-qualified # 👨🍳 E4.0 man cook +1F468 1F3FB 200D 1F373 ; fully-qualified # 👨🏻🍳 E4.0 man cook: light skin tone +1F468 1F3FC 200D 1F373 ; fully-qualified # 👨🏼🍳 E4.0 man cook: medium-light skin tone +1F468 1F3FD 200D 1F373 ; fully-qualified # 👨🏽🍳 E4.0 man cook: medium skin tone +1F468 1F3FE 200D 1F373 ; fully-qualified # 👨🏾🍳 E4.0 man cook: medium-dark skin tone +1F468 1F3FF 200D 1F373 ; fully-qualified # 👨🏿🍳 E4.0 man cook: dark skin tone +1F469 200D 1F373 ; fully-qualified # 👩🍳 E4.0 woman cook +1F469 1F3FB 200D 1F373 ; fully-qualified # 👩🏻🍳 E4.0 woman cook: light skin tone +1F469 1F3FC 200D 1F373 ; fully-qualified # 👩🏼🍳 E4.0 woman cook: medium-light skin tone +1F469 1F3FD 200D 1F373 ; fully-qualified # 👩🏽🍳 E4.0 woman cook: medium skin tone +1F469 1F3FE 200D 1F373 ; fully-qualified # 👩🏾🍳 E4.0 woman cook: medium-dark skin tone +1F469 1F3FF 200D 1F373 ; fully-qualified # 👩🏿🍳 E4.0 woman cook: dark skin tone +1F9D1 200D 1F527 ; fully-qualified # 🧑🔧 E12.1 mechanic +1F9D1 1F3FB 200D 1F527 ; fully-qualified # 🧑🏻🔧 E12.1 mechanic: light skin tone +1F9D1 1F3FC 200D 1F527 ; fully-qualified # 🧑🏼🔧 E12.1 mechanic: medium-light skin tone +1F9D1 1F3FD 200D 1F527 ; fully-qualified # 🧑🏽🔧 E12.1 mechanic: medium skin tone +1F9D1 1F3FE 200D 1F527 ; fully-qualified # 🧑🏾🔧 E12.1 mechanic: medium-dark skin tone +1F9D1 1F3FF 200D 1F527 ; fully-qualified # 🧑🏿🔧 E12.1 mechanic: dark skin tone +1F468 200D 1F527 ; fully-qualified # 👨🔧 E4.0 man mechanic +1F468 1F3FB 200D 1F527 ; fully-qualified # 👨🏻🔧 E4.0 man mechanic: light skin tone +1F468 1F3FC 200D 1F527 ; fully-qualified # 👨🏼🔧 E4.0 man mechanic: medium-light skin tone +1F468 1F3FD 200D 1F527 ; fully-qualified # 👨🏽🔧 E4.0 man mechanic: medium skin tone +1F468 1F3FE 200D 1F527 ; fully-qualified # 👨🏾🔧 E4.0 man mechanic: medium-dark skin tone +1F468 1F3FF 200D 1F527 ; fully-qualified # 👨🏿🔧 E4.0 man mechanic: dark skin tone +1F469 200D 1F527 ; fully-qualified # 👩🔧 E4.0 woman mechanic +1F469 1F3FB 200D 1F527 ; fully-qualified # 👩🏻🔧 E4.0 woman mechanic: light skin tone +1F469 1F3FC 200D 1F527 ; fully-qualified # 👩🏼🔧 E4.0 woman mechanic: medium-light skin tone +1F469 1F3FD 200D 1F527 ; fully-qualified # 👩🏽🔧 E4.0 woman mechanic: medium skin tone +1F469 1F3FE 200D 1F527 ; fully-qualified # 👩🏾🔧 E4.0 woman mechanic: medium-dark skin tone +1F469 1F3FF 200D 1F527 ; fully-qualified # 👩🏿🔧 E4.0 woman mechanic: dark skin tone +1F9D1 200D 1F3ED ; fully-qualified # 🧑🏭 E12.1 factory worker +1F9D1 1F3FB 200D 1F3ED ; fully-qualified # 🧑🏻🏭 E12.1 factory worker: light skin tone +1F9D1 1F3FC 200D 1F3ED ; fully-qualified # 🧑🏼🏭 E12.1 factory worker: medium-light skin tone +1F9D1 1F3FD 200D 1F3ED ; fully-qualified # 🧑🏽🏭 E12.1 factory worker: medium skin tone +1F9D1 1F3FE 200D 1F3ED ; fully-qualified # 🧑🏾🏭 E12.1 factory worker: medium-dark skin tone +1F9D1 1F3FF 200D 1F3ED ; fully-qualified # 🧑🏿🏭 E12.1 factory worker: dark skin tone +1F468 200D 1F3ED ; fully-qualified # 👨🏭 E4.0 man factory worker +1F468 1F3FB 200D 1F3ED ; fully-qualified # 👨🏻🏭 E4.0 man factory worker: light skin tone +1F468 1F3FC 200D 1F3ED ; fully-qualified # 👨🏼🏭 E4.0 man factory worker: medium-light skin tone +1F468 1F3FD 200D 1F3ED ; fully-qualified # 👨🏽🏭 E4.0 man factory worker: medium skin tone +1F468 1F3FE 200D 1F3ED ; fully-qualified # 👨🏾🏭 E4.0 man factory worker: medium-dark skin tone +1F468 1F3FF 200D 1F3ED ; fully-qualified # 👨🏿🏭 E4.0 man factory worker: dark skin tone +1F469 200D 1F3ED ; fully-qualified # 👩🏭 E4.0 woman factory worker +1F469 1F3FB 200D 1F3ED ; fully-qualified # 👩🏻🏭 E4.0 woman factory worker: light skin tone +1F469 1F3FC 200D 1F3ED ; fully-qualified # 👩🏼🏭 E4.0 woman factory worker: medium-light skin tone +1F469 1F3FD 200D 1F3ED ; fully-qualified # 👩🏽🏭 E4.0 woman factory worker: medium skin tone +1F469 1F3FE 200D 1F3ED ; fully-qualified # 👩🏾🏭 E4.0 woman factory worker: medium-dark skin tone +1F469 1F3FF 200D 1F3ED ; fully-qualified # 👩🏿🏭 E4.0 woman factory worker: dark skin tone +1F9D1 200D 1F4BC ; fully-qualified # 🧑💼 E12.1 office worker +1F9D1 1F3FB 200D 1F4BC ; fully-qualified # 🧑🏻💼 E12.1 office worker: light skin tone +1F9D1 1F3FC 200D 1F4BC ; fully-qualified # 🧑🏼💼 E12.1 office worker: medium-light skin tone +1F9D1 1F3FD 200D 1F4BC ; fully-qualified # 🧑🏽💼 E12.1 office worker: medium skin tone +1F9D1 1F3FE 200D 1F4BC ; fully-qualified # 🧑🏾💼 E12.1 office worker: medium-dark skin tone +1F9D1 1F3FF 200D 1F4BC ; fully-qualified # 🧑🏿💼 E12.1 office worker: dark skin tone +1F468 200D 1F4BC ; fully-qualified # 👨💼 E4.0 man office worker +1F468 1F3FB 200D 1F4BC ; fully-qualified # 👨🏻💼 E4.0 man office worker: light skin tone +1F468 1F3FC 200D 1F4BC ; fully-qualified # 👨🏼💼 E4.0 man office worker: medium-light skin tone +1F468 1F3FD 200D 1F4BC ; fully-qualified # 👨🏽💼 E4.0 man office worker: medium skin tone +1F468 1F3FE 200D 1F4BC ; fully-qualified # 👨🏾💼 E4.0 man office worker: medium-dark skin tone +1F468 1F3FF 200D 1F4BC ; fully-qualified # 👨🏿💼 E4.0 man office worker: dark skin tone +1F469 200D 1F4BC ; fully-qualified # 👩💼 E4.0 woman office worker +1F469 1F3FB 200D 1F4BC ; fully-qualified # 👩🏻💼 E4.0 woman office worker: light skin tone +1F469 1F3FC 200D 1F4BC ; fully-qualified # 👩🏼💼 E4.0 woman office worker: medium-light skin tone +1F469 1F3FD 200D 1F4BC ; fully-qualified # 👩🏽💼 E4.0 woman office worker: medium skin tone +1F469 1F3FE 200D 1F4BC ; fully-qualified # 👩🏾💼 E4.0 woman office worker: medium-dark skin tone +1F469 1F3FF 200D 1F4BC ; fully-qualified # 👩🏿💼 E4.0 woman office worker: dark skin tone +1F9D1 200D 1F52C ; fully-qualified # 🧑🔬 E12.1 scientist +1F9D1 1F3FB 200D 1F52C ; fully-qualified # 🧑🏻🔬 E12.1 scientist: light skin tone +1F9D1 1F3FC 200D 1F52C ; fully-qualified # 🧑🏼🔬 E12.1 scientist: medium-light skin tone +1F9D1 1F3FD 200D 1F52C ; fully-qualified # 🧑🏽🔬 E12.1 scientist: medium skin tone +1F9D1 1F3FE 200D 1F52C ; fully-qualified # 🧑🏾🔬 E12.1 scientist: medium-dark skin tone +1F9D1 1F3FF 200D 1F52C ; fully-qualified # 🧑🏿🔬 E12.1 scientist: dark skin tone +1F468 200D 1F52C ; fully-qualified # 👨🔬 E4.0 man scientist +1F468 1F3FB 200D 1F52C ; fully-qualified # 👨🏻🔬 E4.0 man scientist: light skin tone +1F468 1F3FC 200D 1F52C ; fully-qualified # 👨🏼🔬 E4.0 man scientist: medium-light skin tone +1F468 1F3FD 200D 1F52C ; fully-qualified # 👨🏽🔬 E4.0 man scientist: medium skin tone +1F468 1F3FE 200D 1F52C ; fully-qualified # 👨🏾🔬 E4.0 man scientist: medium-dark skin tone +1F468 1F3FF 200D 1F52C ; fully-qualified # 👨🏿🔬 E4.0 man scientist: dark skin tone +1F469 200D 1F52C ; fully-qualified # 👩🔬 E4.0 woman scientist +1F469 1F3FB 200D 1F52C ; fully-qualified # 👩🏻🔬 E4.0 woman scientist: light skin tone +1F469 1F3FC 200D 1F52C ; fully-qualified # 👩🏼🔬 E4.0 woman scientist: medium-light skin tone +1F469 1F3FD 200D 1F52C ; fully-qualified # 👩🏽🔬 E4.0 woman scientist: medium skin tone +1F469 1F3FE 200D 1F52C ; fully-qualified # 👩🏾🔬 E4.0 woman scientist: medium-dark skin tone +1F469 1F3FF 200D 1F52C ; fully-qualified # 👩🏿🔬 E4.0 woman scientist: dark skin tone +1F9D1 200D 1F4BB ; fully-qualified # 🧑💻 E12.1 technologist +1F9D1 1F3FB 200D 1F4BB ; fully-qualified # 🧑🏻💻 E12.1 technologist: light skin tone +1F9D1 1F3FC 200D 1F4BB ; fully-qualified # 🧑🏼💻 E12.1 technologist: medium-light skin tone +1F9D1 1F3FD 200D 1F4BB ; fully-qualified # 🧑🏽💻 E12.1 technologist: medium skin tone +1F9D1 1F3FE 200D 1F4BB ; fully-qualified # 🧑🏾💻 E12.1 technologist: medium-dark skin tone +1F9D1 1F3FF 200D 1F4BB ; fully-qualified # 🧑🏿💻 E12.1 technologist: dark skin tone +1F468 200D 1F4BB ; fully-qualified # 👨💻 E4.0 man technologist +1F468 1F3FB 200D 1F4BB ; fully-qualified # 👨🏻💻 E4.0 man technologist: light skin tone +1F468 1F3FC 200D 1F4BB ; fully-qualified # 👨🏼💻 E4.0 man technologist: medium-light skin tone +1F468 1F3FD 200D 1F4BB ; fully-qualified # 👨🏽💻 E4.0 man technologist: medium skin tone +1F468 1F3FE 200D 1F4BB ; fully-qualified # 👨🏾💻 E4.0 man technologist: medium-dark skin tone +1F468 1F3FF 200D 1F4BB ; fully-qualified # 👨🏿💻 E4.0 man technologist: dark skin tone +1F469 200D 1F4BB ; fully-qualified # 👩💻 E4.0 woman technologist +1F469 1F3FB 200D 1F4BB ; fully-qualified # 👩🏻💻 E4.0 woman technologist: light skin tone +1F469 1F3FC 200D 1F4BB ; fully-qualified # 👩🏼💻 E4.0 woman technologist: medium-light skin tone +1F469 1F3FD 200D 1F4BB ; fully-qualified # 👩🏽💻 E4.0 woman technologist: medium skin tone +1F469 1F3FE 200D 1F4BB ; fully-qualified # 👩🏾💻 E4.0 woman technologist: medium-dark skin tone +1F469 1F3FF 200D 1F4BB ; fully-qualified # 👩🏿💻 E4.0 woman technologist: dark skin tone +1F9D1 200D 1F3A4 ; fully-qualified # 🧑🎤 E12.1 singer +1F9D1 1F3FB 200D 1F3A4 ; fully-qualified # 🧑🏻🎤 E12.1 singer: light skin tone +1F9D1 1F3FC 200D 1F3A4 ; fully-qualified # 🧑🏼🎤 E12.1 singer: medium-light skin tone +1F9D1 1F3FD 200D 1F3A4 ; fully-qualified # 🧑🏽🎤 E12.1 singer: medium skin tone +1F9D1 1F3FE 200D 1F3A4 ; fully-qualified # 🧑🏾🎤 E12.1 singer: medium-dark skin tone +1F9D1 1F3FF 200D 1F3A4 ; fully-qualified # 🧑🏿🎤 E12.1 singer: dark skin tone +1F468 200D 1F3A4 ; fully-qualified # 👨🎤 E4.0 man singer +1F468 1F3FB 200D 1F3A4 ; fully-qualified # 👨🏻🎤 E4.0 man singer: light skin tone +1F468 1F3FC 200D 1F3A4 ; fully-qualified # 👨🏼🎤 E4.0 man singer: medium-light skin tone +1F468 1F3FD 200D 1F3A4 ; fully-qualified # 👨🏽🎤 E4.0 man singer: medium skin tone +1F468 1F3FE 200D 1F3A4 ; fully-qualified # 👨🏾🎤 E4.0 man singer: medium-dark skin tone +1F468 1F3FF 200D 1F3A4 ; fully-qualified # 👨🏿🎤 E4.0 man singer: dark skin tone +1F469 200D 1F3A4 ; fully-qualified # 👩🎤 E4.0 woman singer +1F469 1F3FB 200D 1F3A4 ; fully-qualified # 👩🏻🎤 E4.0 woman singer: light skin tone +1F469 1F3FC 200D 1F3A4 ; fully-qualified # 👩🏼🎤 E4.0 woman singer: medium-light skin tone +1F469 1F3FD 200D 1F3A4 ; fully-qualified # 👩🏽🎤 E4.0 woman singer: medium skin tone +1F469 1F3FE 200D 1F3A4 ; fully-qualified # 👩🏾🎤 E4.0 woman singer: medium-dark skin tone +1F469 1F3FF 200D 1F3A4 ; fully-qualified # 👩🏿🎤 E4.0 woman singer: dark skin tone +1F9D1 200D 1F3A8 ; fully-qualified # 🧑🎨 E12.1 artist +1F9D1 1F3FB 200D 1F3A8 ; fully-qualified # 🧑🏻🎨 E12.1 artist: light skin tone +1F9D1 1F3FC 200D 1F3A8 ; fully-qualified # 🧑🏼🎨 E12.1 artist: medium-light skin tone +1F9D1 1F3FD 200D 1F3A8 ; fully-qualified # 🧑🏽🎨 E12.1 artist: medium skin tone +1F9D1 1F3FE 200D 1F3A8 ; fully-qualified # 🧑🏾🎨 E12.1 artist: medium-dark skin tone +1F9D1 1F3FF 200D 1F3A8 ; fully-qualified # 🧑🏿🎨 E12.1 artist: dark skin tone +1F468 200D 1F3A8 ; fully-qualified # 👨🎨 E4.0 man artist +1F468 1F3FB 200D 1F3A8 ; fully-qualified # 👨🏻🎨 E4.0 man artist: light skin tone +1F468 1F3FC 200D 1F3A8 ; fully-qualified # 👨🏼🎨 E4.0 man artist: medium-light skin tone +1F468 1F3FD 200D 1F3A8 ; fully-qualified # 👨🏽🎨 E4.0 man artist: medium skin tone +1F468 1F3FE 200D 1F3A8 ; fully-qualified # 👨🏾🎨 E4.0 man artist: medium-dark skin tone +1F468 1F3FF 200D 1F3A8 ; fully-qualified # 👨🏿🎨 E4.0 man artist: dark skin tone +1F469 200D 1F3A8 ; fully-qualified # 👩🎨 E4.0 woman artist +1F469 1F3FB 200D 1F3A8 ; fully-qualified # 👩🏻🎨 E4.0 woman artist: light skin tone +1F469 1F3FC 200D 1F3A8 ; fully-qualified # 👩🏼🎨 E4.0 woman artist: medium-light skin tone +1F469 1F3FD 200D 1F3A8 ; fully-qualified # 👩🏽🎨 E4.0 woman artist: medium skin tone +1F469 1F3FE 200D 1F3A8 ; fully-qualified # 👩🏾🎨 E4.0 woman artist: medium-dark skin tone +1F469 1F3FF 200D 1F3A8 ; fully-qualified # 👩🏿🎨 E4.0 woman artist: dark skin tone +1F9D1 200D 2708 FE0F ; fully-qualified # 🧑✈️ E12.1 pilot +1F9D1 200D 2708 ; minimally-qualified # 🧑✈ E12.1 pilot +1F9D1 1F3FB 200D 2708 FE0F ; fully-qualified # 🧑🏻✈️ E12.1 pilot: light skin tone +1F9D1 1F3FB 200D 2708 ; minimally-qualified # 🧑🏻✈ E12.1 pilot: light skin tone +1F9D1 1F3FC 200D 2708 FE0F ; fully-qualified # 🧑🏼✈️ E12.1 pilot: medium-light skin tone +1F9D1 1F3FC 200D 2708 ; minimally-qualified # 🧑🏼✈ E12.1 pilot: medium-light skin tone +1F9D1 1F3FD 200D 2708 FE0F ; fully-qualified # 🧑🏽✈️ E12.1 pilot: medium skin tone +1F9D1 1F3FD 200D 2708 ; minimally-qualified # 🧑🏽✈ E12.1 pilot: medium skin tone +1F9D1 1F3FE 200D 2708 FE0F ; fully-qualified # 🧑🏾✈️ E12.1 pilot: medium-dark skin tone +1F9D1 1F3FE 200D 2708 ; minimally-qualified # 🧑🏾✈ E12.1 pilot: medium-dark skin tone +1F9D1 1F3FF 200D 2708 FE0F ; fully-qualified # 🧑🏿✈️ E12.1 pilot: dark skin tone +1F9D1 1F3FF 200D 2708 ; minimally-qualified # 🧑🏿✈ E12.1 pilot: dark skin tone +1F468 200D 2708 FE0F ; fully-qualified # 👨✈️ E4.0 man pilot +1F468 200D 2708 ; minimally-qualified # 👨✈ E4.0 man pilot +1F468 1F3FB 200D 2708 FE0F ; fully-qualified # 👨🏻✈️ E4.0 man pilot: light skin tone +1F468 1F3FB 200D 2708 ; minimally-qualified # 👨🏻✈ E4.0 man pilot: light skin tone +1F468 1F3FC 200D 2708 FE0F ; fully-qualified # 👨🏼✈️ E4.0 man pilot: medium-light skin tone +1F468 1F3FC 200D 2708 ; minimally-qualified # 👨🏼✈ E4.0 man pilot: medium-light skin tone +1F468 1F3FD 200D 2708 FE0F ; fully-qualified # 👨🏽✈️ E4.0 man pilot: medium skin tone +1F468 1F3FD 200D 2708 ; minimally-qualified # 👨🏽✈ E4.0 man pilot: medium skin tone +1F468 1F3FE 200D 2708 FE0F ; fully-qualified # 👨🏾✈️ E4.0 man pilot: medium-dark skin tone +1F468 1F3FE 200D 2708 ; minimally-qualified # 👨🏾✈ E4.0 man pilot: medium-dark skin tone +1F468 1F3FF 200D 2708 FE0F ; fully-qualified # 👨🏿✈️ E4.0 man pilot: dark skin tone +1F468 1F3FF 200D 2708 ; minimally-qualified # 👨🏿✈ E4.0 man pilot: dark skin tone +1F469 200D 2708 FE0F ; fully-qualified # 👩✈️ E4.0 woman pilot +1F469 200D 2708 ; minimally-qualified # 👩✈ E4.0 woman pilot +1F469 1F3FB 200D 2708 FE0F ; fully-qualified # 👩🏻✈️ E4.0 woman pilot: light skin tone +1F469 1F3FB 200D 2708 ; minimally-qualified # 👩🏻✈ E4.0 woman pilot: light skin tone +1F469 1F3FC 200D 2708 FE0F ; fully-qualified # 👩🏼✈️ E4.0 woman pilot: medium-light skin tone +1F469 1F3FC 200D 2708 ; minimally-qualified # 👩🏼✈ E4.0 woman pilot: medium-light skin tone +1F469 1F3FD 200D 2708 FE0F ; fully-qualified # 👩🏽✈️ E4.0 woman pilot: medium skin tone +1F469 1F3FD 200D 2708 ; minimally-qualified # 👩🏽✈ E4.0 woman pilot: medium skin tone +1F469 1F3FE 200D 2708 FE0F ; fully-qualified # 👩🏾✈️ E4.0 woman pilot: medium-dark skin tone +1F469 1F3FE 200D 2708 ; minimally-qualified # 👩🏾✈ E4.0 woman pilot: medium-dark skin tone +1F469 1F3FF 200D 2708 FE0F ; fully-qualified # 👩🏿✈️ E4.0 woman pilot: dark skin tone +1F469 1F3FF 200D 2708 ; minimally-qualified # 👩🏿✈ E4.0 woman pilot: dark skin tone +1F9D1 200D 1F680 ; fully-qualified # 🧑🚀 E12.1 astronaut +1F9D1 1F3FB 200D 1F680 ; fully-qualified # 🧑🏻🚀 E12.1 astronaut: light skin tone +1F9D1 1F3FC 200D 1F680 ; fully-qualified # 🧑🏼🚀 E12.1 astronaut: medium-light skin tone +1F9D1 1F3FD 200D 1F680 ; fully-qualified # 🧑🏽🚀 E12.1 astronaut: medium skin tone +1F9D1 1F3FE 200D 1F680 ; fully-qualified # 🧑🏾🚀 E12.1 astronaut: medium-dark skin tone +1F9D1 1F3FF 200D 1F680 ; fully-qualified # 🧑🏿🚀 E12.1 astronaut: dark skin tone +1F468 200D 1F680 ; fully-qualified # 👨🚀 E4.0 man astronaut +1F468 1F3FB 200D 1F680 ; fully-qualified # 👨🏻🚀 E4.0 man astronaut: light skin tone +1F468 1F3FC 200D 1F680 ; fully-qualified # 👨🏼🚀 E4.0 man astronaut: medium-light skin tone +1F468 1F3FD 200D 1F680 ; fully-qualified # 👨🏽🚀 E4.0 man astronaut: medium skin tone +1F468 1F3FE 200D 1F680 ; fully-qualified # 👨🏾🚀 E4.0 man astronaut: medium-dark skin tone +1F468 1F3FF 200D 1F680 ; fully-qualified # 👨🏿🚀 E4.0 man astronaut: dark skin tone +1F469 200D 1F680 ; fully-qualified # 👩🚀 E4.0 woman astronaut +1F469 1F3FB 200D 1F680 ; fully-qualified # 👩🏻🚀 E4.0 woman astronaut: light skin tone +1F469 1F3FC 200D 1F680 ; fully-qualified # 👩🏼🚀 E4.0 woman astronaut: medium-light skin tone +1F469 1F3FD 200D 1F680 ; fully-qualified # 👩🏽🚀 E4.0 woman astronaut: medium skin tone +1F469 1F3FE 200D 1F680 ; fully-qualified # 👩🏾🚀 E4.0 woman astronaut: medium-dark skin tone +1F469 1F3FF 200D 1F680 ; fully-qualified # 👩🏿🚀 E4.0 woman astronaut: dark skin tone +1F9D1 200D 1F692 ; fully-qualified # 🧑🚒 E12.1 firefighter +1F9D1 1F3FB 200D 1F692 ; fully-qualified # 🧑🏻🚒 E12.1 firefighter: light skin tone +1F9D1 1F3FC 200D 1F692 ; fully-qualified # 🧑🏼🚒 E12.1 firefighter: medium-light skin tone +1F9D1 1F3FD 200D 1F692 ; fully-qualified # 🧑🏽🚒 E12.1 firefighter: medium skin tone +1F9D1 1F3FE 200D 1F692 ; fully-qualified # 🧑🏾🚒 E12.1 firefighter: medium-dark skin tone +1F9D1 1F3FF 200D 1F692 ; fully-qualified # 🧑🏿🚒 E12.1 firefighter: dark skin tone +1F468 200D 1F692 ; fully-qualified # 👨🚒 E4.0 man firefighter +1F468 1F3FB 200D 1F692 ; fully-qualified # 👨🏻🚒 E4.0 man firefighter: light skin tone +1F468 1F3FC 200D 1F692 ; fully-qualified # 👨🏼🚒 E4.0 man firefighter: medium-light skin tone +1F468 1F3FD 200D 1F692 ; fully-qualified # 👨🏽🚒 E4.0 man firefighter: medium skin tone +1F468 1F3FE 200D 1F692 ; fully-qualified # 👨🏾🚒 E4.0 man firefighter: medium-dark skin tone +1F468 1F3FF 200D 1F692 ; fully-qualified # 👨🏿🚒 E4.0 man firefighter: dark skin tone +1F469 200D 1F692 ; fully-qualified # 👩🚒 E4.0 woman firefighter +1F469 1F3FB 200D 1F692 ; fully-qualified # 👩🏻🚒 E4.0 woman firefighter: light skin tone +1F469 1F3FC 200D 1F692 ; fully-qualified # 👩🏼🚒 E4.0 woman firefighter: medium-light skin tone +1F469 1F3FD 200D 1F692 ; fully-qualified # 👩🏽🚒 E4.0 woman firefighter: medium skin tone +1F469 1F3FE 200D 1F692 ; fully-qualified # 👩🏾🚒 E4.0 woman firefighter: medium-dark skin tone +1F469 1F3FF 200D 1F692 ; fully-qualified # 👩🏿🚒 E4.0 woman firefighter: dark skin tone +1F46E ; fully-qualified # 👮 E0.6 police officer +1F46E 1F3FB ; fully-qualified # 👮🏻 E1.0 police officer: light skin tone +1F46E 1F3FC ; fully-qualified # 👮🏼 E1.0 police officer: medium-light skin tone +1F46E 1F3FD ; fully-qualified # 👮🏽 E1.0 police officer: medium skin tone +1F46E 1F3FE ; fully-qualified # 👮🏾 E1.0 police officer: medium-dark skin tone +1F46E 1F3FF ; fully-qualified # 👮🏿 E1.0 police officer: dark skin tone +1F46E 200D 2642 FE0F ; fully-qualified # 👮♂️ E4.0 man police officer +1F46E 200D 2642 ; minimally-qualified # 👮♂ E4.0 man police officer +1F46E 1F3FB 200D 2642 FE0F ; fully-qualified # 👮🏻♂️ E4.0 man police officer: light skin tone +1F46E 1F3FB 200D 2642 ; minimally-qualified # 👮🏻♂ E4.0 man police officer: light skin tone +1F46E 1F3FC 200D 2642 FE0F ; fully-qualified # 👮🏼♂️ E4.0 man police officer: medium-light skin tone +1F46E 1F3FC 200D 2642 ; minimally-qualified # 👮🏼♂ E4.0 man police officer: medium-light skin tone +1F46E 1F3FD 200D 2642 FE0F ; fully-qualified # 👮🏽♂️ E4.0 man police officer: medium skin tone +1F46E 1F3FD 200D 2642 ; minimally-qualified # 👮🏽♂ E4.0 man police officer: medium skin tone +1F46E 1F3FE 200D 2642 FE0F ; fully-qualified # 👮🏾♂️ E4.0 man police officer: medium-dark skin tone +1F46E 1F3FE 200D 2642 ; minimally-qualified # 👮🏾♂ E4.0 man police officer: medium-dark skin tone +1F46E 1F3FF 200D 2642 FE0F ; fully-qualified # 👮🏿♂️ E4.0 man police officer: dark skin tone +1F46E 1F3FF 200D 2642 ; minimally-qualified # 👮🏿♂ E4.0 man police officer: dark skin tone +1F46E 200D 2640 FE0F ; fully-qualified # 👮♀️ E4.0 woman police officer +1F46E 200D 2640 ; minimally-qualified # 👮♀ E4.0 woman police officer +1F46E 1F3FB 200D 2640 FE0F ; fully-qualified # 👮🏻♀️ E4.0 woman police officer: light skin tone +1F46E 1F3FB 200D 2640 ; minimally-qualified # 👮🏻♀ E4.0 woman police officer: light skin tone +1F46E 1F3FC 200D 2640 FE0F ; fully-qualified # 👮🏼♀️ E4.0 woman police officer: medium-light skin tone +1F46E 1F3FC 200D 2640 ; minimally-qualified # 👮🏼♀ E4.0 woman police officer: medium-light skin tone +1F46E 1F3FD 200D 2640 FE0F ; fully-qualified # 👮🏽♀️ E4.0 woman police officer: medium skin tone +1F46E 1F3FD 200D 2640 ; minimally-qualified # 👮🏽♀ E4.0 woman police officer: medium skin tone +1F46E 1F3FE 200D 2640 FE0F ; fully-qualified # 👮🏾♀️ E4.0 woman police officer: medium-dark skin tone +1F46E 1F3FE 200D 2640 ; minimally-qualified # 👮🏾♀ E4.0 woman police officer: medium-dark skin tone +1F46E 1F3FF 200D 2640 FE0F ; fully-qualified # 👮🏿♀️ E4.0 woman police officer: dark skin tone +1F46E 1F3FF 200D 2640 ; minimally-qualified # 👮🏿♀ E4.0 woman police officer: dark skin tone +1F575 FE0F ; fully-qualified # 🕵️ E0.7 detective +1F575 ; unqualified # 🕵 E0.7 detective +1F575 1F3FB ; fully-qualified # 🕵🏻 E2.0 detective: light skin tone +1F575 1F3FC ; fully-qualified # 🕵🏼 E2.0 detective: medium-light skin tone +1F575 1F3FD ; fully-qualified # 🕵🏽 E2.0 detective: medium skin tone +1F575 1F3FE ; fully-qualified # 🕵🏾 E2.0 detective: medium-dark skin tone +1F575 1F3FF ; fully-qualified # 🕵🏿 E2.0 detective: dark skin tone +1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️♂️ E4.0 man detective +1F575 200D 2642 FE0F ; unqualified # 🕵♂️ E4.0 man detective +1F575 FE0F 200D 2642 ; unqualified # 🕵️♂ E4.0 man detective +1F575 200D 2642 ; unqualified # 🕵♂ E4.0 man detective +1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻♂️ E4.0 man detective: light skin tone +1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻♂ E4.0 man detective: light skin tone +1F575 1F3FC 200D 2642 FE0F ; fully-qualified # 🕵🏼♂️ E4.0 man detective: medium-light skin tone +1F575 1F3FC 200D 2642 ; minimally-qualified # 🕵🏼♂ E4.0 man detective: medium-light skin tone +1F575 1F3FD 200D 2642 FE0F ; fully-qualified # 🕵🏽♂️ E4.0 man detective: medium skin tone +1F575 1F3FD 200D 2642 ; minimally-qualified # 🕵🏽♂ E4.0 man detective: medium skin tone +1F575 1F3FE 200D 2642 FE0F ; fully-qualified # 🕵🏾♂️ E4.0 man detective: medium-dark skin tone +1F575 1F3FE 200D 2642 ; minimally-qualified # 🕵🏾♂ E4.0 man detective: medium-dark skin tone +1F575 1F3FF 200D 2642 FE0F ; fully-qualified # 🕵🏿♂️ E4.0 man detective: dark skin tone +1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿♂ E4.0 man detective: dark skin tone +1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️♀️ E4.0 woman detective +1F575 200D 2640 FE0F ; unqualified # 🕵♀️ E4.0 woman detective +1F575 FE0F 200D 2640 ; unqualified # 🕵️♀ E4.0 woman detective +1F575 200D 2640 ; unqualified # 🕵♀ E4.0 woman detective +1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻♀️ E4.0 woman detective: light skin tone +1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻♀ E4.0 woman detective: light skin tone +1F575 1F3FC 200D 2640 FE0F ; fully-qualified # 🕵🏼♀️ E4.0 woman detective: medium-light skin tone +1F575 1F3FC 200D 2640 ; minimally-qualified # 🕵🏼♀ E4.0 woman detective: medium-light skin tone +1F575 1F3FD 200D 2640 FE0F ; fully-qualified # 🕵🏽♀️ E4.0 woman detective: medium skin tone +1F575 1F3FD 200D 2640 ; minimally-qualified # 🕵🏽♀ E4.0 woman detective: medium skin tone +1F575 1F3FE 200D 2640 FE0F ; fully-qualified # 🕵🏾♀️ E4.0 woman detective: medium-dark skin tone +1F575 1F3FE 200D 2640 ; minimally-qualified # 🕵🏾♀ E4.0 woman detective: medium-dark skin tone +1F575 1F3FF 200D 2640 FE0F ; fully-qualified # 🕵🏿♀️ E4.0 woman detective: dark skin tone +1F575 1F3FF 200D 2640 ; minimally-qualified # 🕵🏿♀ E4.0 woman detective: dark skin tone +1F482 ; fully-qualified # 💂 E0.6 guard +1F482 1F3FB ; fully-qualified # 💂🏻 E1.0 guard: light skin tone +1F482 1F3FC ; fully-qualified # 💂🏼 E1.0 guard: medium-light skin tone +1F482 1F3FD ; fully-qualified # 💂🏽 E1.0 guard: medium skin tone +1F482 1F3FE ; fully-qualified # 💂🏾 E1.0 guard: medium-dark skin tone +1F482 1F3FF ; fully-qualified # 💂🏿 E1.0 guard: dark skin tone +1F482 200D 2642 FE0F ; fully-qualified # 💂♂️ E4.0 man guard +1F482 200D 2642 ; minimally-qualified # 💂♂ E4.0 man guard +1F482 1F3FB 200D 2642 FE0F ; fully-qualified # 💂🏻♂️ E4.0 man guard: light skin tone +1F482 1F3FB 200D 2642 ; minimally-qualified # 💂🏻♂ E4.0 man guard: light skin tone +1F482 1F3FC 200D 2642 FE0F ; fully-qualified # 💂🏼♂️ E4.0 man guard: medium-light skin tone +1F482 1F3FC 200D 2642 ; minimally-qualified # 💂🏼♂ E4.0 man guard: medium-light skin tone +1F482 1F3FD 200D 2642 FE0F ; fully-qualified # 💂🏽♂️ E4.0 man guard: medium skin tone +1F482 1F3FD 200D 2642 ; minimally-qualified # 💂🏽♂ E4.0 man guard: medium skin tone +1F482 1F3FE 200D 2642 FE0F ; fully-qualified # 💂🏾♂️ E4.0 man guard: medium-dark skin tone +1F482 1F3FE 200D 2642 ; minimally-qualified # 💂🏾♂ E4.0 man guard: medium-dark skin tone +1F482 1F3FF 200D 2642 FE0F ; fully-qualified # 💂🏿♂️ E4.0 man guard: dark skin tone +1F482 1F3FF 200D 2642 ; minimally-qualified # 💂🏿♂ E4.0 man guard: dark skin tone +1F482 200D 2640 FE0F ; fully-qualified # 💂♀️ E4.0 woman guard +1F482 200D 2640 ; minimally-qualified # 💂♀ E4.0 woman guard +1F482 1F3FB 200D 2640 FE0F ; fully-qualified # 💂🏻♀️ E4.0 woman guard: light skin tone +1F482 1F3FB 200D 2640 ; minimally-qualified # 💂🏻♀ E4.0 woman guard: light skin tone +1F482 1F3FC 200D 2640 FE0F ; fully-qualified # 💂🏼♀️ E4.0 woman guard: medium-light skin tone +1F482 1F3FC 200D 2640 ; minimally-qualified # 💂🏼♀ E4.0 woman guard: medium-light skin tone +1F482 1F3FD 200D 2640 FE0F ; fully-qualified # 💂🏽♀️ E4.0 woman guard: medium skin tone +1F482 1F3FD 200D 2640 ; minimally-qualified # 💂🏽♀ E4.0 woman guard: medium skin tone +1F482 1F3FE 200D 2640 FE0F ; fully-qualified # 💂🏾♀️ E4.0 woman guard: medium-dark skin tone +1F482 1F3FE 200D 2640 ; minimally-qualified # 💂🏾♀ E4.0 woman guard: medium-dark skin tone +1F482 1F3FF 200D 2640 FE0F ; fully-qualified # 💂🏿♀️ E4.0 woman guard: dark skin tone +1F482 1F3FF 200D 2640 ; minimally-qualified # 💂🏿♀ E4.0 woman guard: dark skin tone +1F477 ; fully-qualified # 👷 E0.6 construction worker +1F477 1F3FB ; fully-qualified # 👷🏻 E1.0 construction worker: light skin tone +1F477 1F3FC ; fully-qualified # 👷🏼 E1.0 construction worker: medium-light skin tone +1F477 1F3FD ; fully-qualified # 👷🏽 E1.0 construction worker: medium skin tone +1F477 1F3FE ; fully-qualified # 👷🏾 E1.0 construction worker: medium-dark skin tone +1F477 1F3FF ; fully-qualified # 👷🏿 E1.0 construction worker: dark skin tone +1F477 200D 2642 FE0F ; fully-qualified # 👷♂️ E4.0 man construction worker +1F477 200D 2642 ; minimally-qualified # 👷♂ E4.0 man construction worker +1F477 1F3FB 200D 2642 FE0F ; fully-qualified # 👷🏻♂️ E4.0 man construction worker: light skin tone +1F477 1F3FB 200D 2642 ; minimally-qualified # 👷🏻♂ E4.0 man construction worker: light skin tone +1F477 1F3FC 200D 2642 FE0F ; fully-qualified # 👷🏼♂️ E4.0 man construction worker: medium-light skin tone +1F477 1F3FC 200D 2642 ; minimally-qualified # 👷🏼♂ E4.0 man construction worker: medium-light skin tone +1F477 1F3FD 200D 2642 FE0F ; fully-qualified # 👷🏽♂️ E4.0 man construction worker: medium skin tone +1F477 1F3FD 200D 2642 ; minimally-qualified # 👷🏽♂ E4.0 man construction worker: medium skin tone +1F477 1F3FE 200D 2642 FE0F ; fully-qualified # 👷🏾♂️ E4.0 man construction worker: medium-dark skin tone +1F477 1F3FE 200D 2642 ; minimally-qualified # 👷🏾♂ E4.0 man construction worker: medium-dark skin tone +1F477 1F3FF 200D 2642 FE0F ; fully-qualified # 👷🏿♂️ E4.0 man construction worker: dark skin tone +1F477 1F3FF 200D 2642 ; minimally-qualified # 👷🏿♂ E4.0 man construction worker: dark skin tone +1F477 200D 2640 FE0F ; fully-qualified # 👷♀️ E4.0 woman construction worker +1F477 200D 2640 ; minimally-qualified # 👷♀ E4.0 woman construction worker +1F477 1F3FB 200D 2640 FE0F ; fully-qualified # 👷🏻♀️ E4.0 woman construction worker: light skin tone +1F477 1F3FB 200D 2640 ; minimally-qualified # 👷🏻♀ E4.0 woman construction worker: light skin tone +1F477 1F3FC 200D 2640 FE0F ; fully-qualified # 👷🏼♀️ E4.0 woman construction worker: medium-light skin tone +1F477 1F3FC 200D 2640 ; minimally-qualified # 👷🏼♀ E4.0 woman construction worker: medium-light skin tone +1F477 1F3FD 200D 2640 FE0F ; fully-qualified # 👷🏽♀️ E4.0 woman construction worker: medium skin tone +1F477 1F3FD 200D 2640 ; minimally-qualified # 👷🏽♀ E4.0 woman construction worker: medium skin tone +1F477 1F3FE 200D 2640 FE0F ; fully-qualified # 👷🏾♀️ E4.0 woman construction worker: medium-dark skin tone +1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾♀ E4.0 woman construction worker: medium-dark skin tone +1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿♀️ E4.0 woman construction worker: dark skin tone +1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿♀ E4.0 woman construction worker: dark skin tone +1F934 ; fully-qualified # 🤴 E3.0 prince +1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone +1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone +1F934 1F3FD ; fully-qualified # 🤴🏽 E3.0 prince: medium skin tone +1F934 1F3FE ; fully-qualified # 🤴🏾 E3.0 prince: medium-dark skin tone +1F934 1F3FF ; fully-qualified # 🤴🏿 E3.0 prince: dark skin tone +1F478 ; fully-qualified # 👸 E0.6 princess +1F478 1F3FB ; fully-qualified # 👸🏻 E1.0 princess: light skin tone +1F478 1F3FC ; fully-qualified # 👸🏼 E1.0 princess: medium-light skin tone +1F478 1F3FD ; fully-qualified # 👸🏽 E1.0 princess: medium skin tone +1F478 1F3FE ; fully-qualified # 👸🏾 E1.0 princess: medium-dark skin tone +1F478 1F3FF ; fully-qualified # 👸🏿 E1.0 princess: dark skin tone +1F473 ; fully-qualified # 👳 E0.6 person wearing turban +1F473 1F3FB ; fully-qualified # 👳🏻 E1.0 person wearing turban: light skin tone +1F473 1F3FC ; fully-qualified # 👳🏼 E1.0 person wearing turban: medium-light skin tone +1F473 1F3FD ; fully-qualified # 👳🏽 E1.0 person wearing turban: medium skin tone +1F473 1F3FE ; fully-qualified # 👳🏾 E1.0 person wearing turban: medium-dark skin tone +1F473 1F3FF ; fully-qualified # 👳🏿 E1.0 person wearing turban: dark skin tone +1F473 200D 2642 FE0F ; fully-qualified # 👳♂️ E4.0 man wearing turban +1F473 200D 2642 ; minimally-qualified # 👳♂ E4.0 man wearing turban +1F473 1F3FB 200D 2642 FE0F ; fully-qualified # 👳🏻♂️ E4.0 man wearing turban: light skin tone +1F473 1F3FB 200D 2642 ; minimally-qualified # 👳🏻♂ E4.0 man wearing turban: light skin tone +1F473 1F3FC 200D 2642 FE0F ; fully-qualified # 👳🏼♂️ E4.0 man wearing turban: medium-light skin tone +1F473 1F3FC 200D 2642 ; minimally-qualified # 👳🏼♂ E4.0 man wearing turban: medium-light skin tone +1F473 1F3FD 200D 2642 FE0F ; fully-qualified # 👳🏽♂️ E4.0 man wearing turban: medium skin tone +1F473 1F3FD 200D 2642 ; minimally-qualified # 👳🏽♂ E4.0 man wearing turban: medium skin tone +1F473 1F3FE 200D 2642 FE0F ; fully-qualified # 👳🏾♂️ E4.0 man wearing turban: medium-dark skin tone +1F473 1F3FE 200D 2642 ; minimally-qualified # 👳🏾♂ E4.0 man wearing turban: medium-dark skin tone +1F473 1F3FF 200D 2642 FE0F ; fully-qualified # 👳🏿♂️ E4.0 man wearing turban: dark skin tone +1F473 1F3FF 200D 2642 ; minimally-qualified # 👳🏿♂ E4.0 man wearing turban: dark skin tone +1F473 200D 2640 FE0F ; fully-qualified # 👳♀️ E4.0 woman wearing turban +1F473 200D 2640 ; minimally-qualified # 👳♀ E4.0 woman wearing turban +1F473 1F3FB 200D 2640 FE0F ; fully-qualified # 👳🏻♀️ E4.0 woman wearing turban: light skin tone +1F473 1F3FB 200D 2640 ; minimally-qualified # 👳🏻♀ E4.0 woman wearing turban: light skin tone +1F473 1F3FC 200D 2640 FE0F ; fully-qualified # 👳🏼♀️ E4.0 woman wearing turban: medium-light skin tone +1F473 1F3FC 200D 2640 ; minimally-qualified # 👳🏼♀ E4.0 woman wearing turban: medium-light skin tone +1F473 1F3FD 200D 2640 FE0F ; fully-qualified # 👳🏽♀️ E4.0 woman wearing turban: medium skin tone +1F473 1F3FD 200D 2640 ; minimally-qualified # 👳🏽♀ E4.0 woman wearing turban: medium skin tone +1F473 1F3FE 200D 2640 FE0F ; fully-qualified # 👳🏾♀️ E4.0 woman wearing turban: medium-dark skin tone +1F473 1F3FE 200D 2640 ; minimally-qualified # 👳🏾♀ E4.0 woman wearing turban: medium-dark skin tone +1F473 1F3FF 200D 2640 FE0F ; fully-qualified # 👳🏿♀️ E4.0 woman wearing turban: dark skin tone +1F473 1F3FF 200D 2640 ; minimally-qualified # 👳🏿♀ E4.0 woman wearing turban: dark skin tone +1F472 ; fully-qualified # 👲 E0.6 man with skullcap +1F472 1F3FB ; fully-qualified # 👲🏻 E1.0 man with skullcap: light skin tone +1F472 1F3FC ; fully-qualified # 👲🏼 E1.0 man with skullcap: medium-light skin tone +1F472 1F3FD ; fully-qualified # 👲🏽 E1.0 man with skullcap: medium skin tone +1F472 1F3FE ; fully-qualified # 👲🏾 E1.0 man with skullcap: medium-dark skin tone +1F472 1F3FF ; fully-qualified # 👲🏿 E1.0 man with skullcap: dark skin tone +1F9D5 ; fully-qualified # 🧕 E5.0 woman with headscarf +1F9D5 1F3FB ; fully-qualified # 🧕🏻 E5.0 woman with headscarf: light skin tone +1F9D5 1F3FC ; fully-qualified # 🧕🏼 E5.0 woman with headscarf: medium-light skin tone +1F9D5 1F3FD ; fully-qualified # 🧕🏽 E5.0 woman with headscarf: medium skin tone +1F9D5 1F3FE ; fully-qualified # 🧕🏾 E5.0 woman with headscarf: medium-dark skin tone +1F9D5 1F3FF ; fully-qualified # 🧕🏿 E5.0 woman with headscarf: dark skin tone +1F935 ; fully-qualified # 🤵 E3.0 man in tuxedo +1F935 1F3FB ; fully-qualified # 🤵🏻 E3.0 man in tuxedo: light skin tone +1F935 1F3FC ; fully-qualified # 🤵🏼 E3.0 man in tuxedo: medium-light skin tone +1F935 1F3FD ; fully-qualified # 🤵🏽 E3.0 man in tuxedo: medium skin tone +1F935 1F3FE ; fully-qualified # 🤵🏾 E3.0 man in tuxedo: medium-dark skin tone +1F935 1F3FF ; fully-qualified # 🤵🏿 E3.0 man in tuxedo: dark skin tone +1F935 200D 2642 FE0F ; fully-qualified # 🤵♂️ E13.0 man in tuxedo +1F935 200D 2642 ; minimally-qualified # 🤵♂ E13.0 man in tuxedo +1F935 1F3FB 200D 2642 FE0F ; fully-qualified # 🤵🏻♂️ E13.0 man in tuxedo: light skin tone +1F935 1F3FB 200D 2642 ; minimally-qualified # 🤵🏻♂ E13.0 man in tuxedo: light skin tone +1F935 1F3FC 200D 2642 FE0F ; fully-qualified # 🤵🏼♂️ E13.0 man in tuxedo: medium-light skin tone +1F935 1F3FC 200D 2642 ; minimally-qualified # 🤵🏼♂ E13.0 man in tuxedo: medium-light skin tone +1F935 1F3FD 200D 2642 FE0F ; fully-qualified # 🤵🏽♂️ E13.0 man in tuxedo: medium skin tone +1F935 1F3FD 200D 2642 ; minimally-qualified # 🤵🏽♂ E13.0 man in tuxedo: medium skin tone +1F935 1F3FE 200D 2642 FE0F ; fully-qualified # 🤵🏾♂️ E13.0 man in tuxedo: medium-dark skin tone +1F935 1F3FE 200D 2642 ; minimally-qualified # 🤵🏾♂ E13.0 man in tuxedo: medium-dark skin tone +1F935 1F3FF 200D 2642 FE0F ; fully-qualified # 🤵🏿♂️ E13.0 man in tuxedo: dark skin tone +1F935 1F3FF 200D 2642 ; minimally-qualified # 🤵🏿♂ E13.0 man in tuxedo: dark skin tone +1F935 200D 2640 FE0F ; fully-qualified # 🤵♀️ E13.0 woman in tuxedo +1F935 200D 2640 ; minimally-qualified # 🤵♀ E13.0 woman in tuxedo +1F935 1F3FB 200D 2640 FE0F ; fully-qualified # 🤵🏻♀️ E13.0 woman in tuxedo: light skin tone +1F935 1F3FB 200D 2640 ; minimally-qualified # 🤵🏻♀ E13.0 woman in tuxedo: light skin tone +1F935 1F3FC 200D 2640 FE0F ; fully-qualified # 🤵🏼♀️ E13.0 woman in tuxedo: medium-light skin tone +1F935 1F3FC 200D 2640 ; minimally-qualified # 🤵🏼♀ E13.0 woman in tuxedo: medium-light skin tone +1F935 1F3FD 200D 2640 FE0F ; fully-qualified # 🤵🏽♀️ E13.0 woman in tuxedo: medium skin tone +1F935 1F3FD 200D 2640 ; minimally-qualified # 🤵🏽♀ E13.0 woman in tuxedo: medium skin tone +1F935 1F3FE 200D 2640 FE0F ; fully-qualified # 🤵🏾♀️ E13.0 woman in tuxedo: medium-dark skin tone +1F935 1F3FE 200D 2640 ; minimally-qualified # 🤵🏾♀ E13.0 woman in tuxedo: medium-dark skin tone +1F935 1F3FF 200D 2640 FE0F ; fully-qualified # 🤵🏿♀️ E13.0 woman in tuxedo: dark skin tone +1F935 1F3FF 200D 2640 ; minimally-qualified # 🤵🏿♀ E13.0 woman in tuxedo: dark skin tone +1F470 ; fully-qualified # 👰 E0.6 bride with veil +1F470 1F3FB ; fully-qualified # 👰🏻 E1.0 bride with veil: light skin tone +1F470 1F3FC ; fully-qualified # 👰🏼 E1.0 bride with veil: medium-light skin tone +1F470 1F3FD ; fully-qualified # 👰🏽 E1.0 bride with veil: medium skin tone +1F470 1F3FE ; fully-qualified # 👰🏾 E1.0 bride with veil: medium-dark skin tone +1F470 1F3FF ; fully-qualified # 👰🏿 E1.0 bride with veil: dark skin tone +1F470 200D 2642 FE0F ; fully-qualified # 👰♂️ E13.0 man with veil +1F470 200D 2642 ; minimally-qualified # 👰♂ E13.0 man with veil +1F470 1F3FB 200D 2642 FE0F ; fully-qualified # 👰🏻♂️ E13.0 man with veil: light skin tone +1F470 1F3FB 200D 2642 ; minimally-qualified # 👰🏻♂ E13.0 man with veil: light skin tone +1F470 1F3FC 200D 2642 FE0F ; fully-qualified # 👰🏼♂️ E13.0 man with veil: medium-light skin tone +1F470 1F3FC 200D 2642 ; minimally-qualified # 👰🏼♂ E13.0 man with veil: medium-light skin tone +1F470 1F3FD 200D 2642 FE0F ; fully-qualified # 👰🏽♂️ E13.0 man with veil: medium skin tone +1F470 1F3FD 200D 2642 ; minimally-qualified # 👰🏽♂ E13.0 man with veil: medium skin tone +1F470 1F3FE 200D 2642 FE0F ; fully-qualified # 👰🏾♂️ E13.0 man with veil: medium-dark skin tone +1F470 1F3FE 200D 2642 ; minimally-qualified # 👰🏾♂ E13.0 man with veil: medium-dark skin tone +1F470 1F3FF 200D 2642 FE0F ; fully-qualified # 👰🏿♂️ E13.0 man with veil: dark skin tone +1F470 1F3FF 200D 2642 ; minimally-qualified # 👰🏿♂ E13.0 man with veil: dark skin tone +1F470 200D 2640 FE0F ; fully-qualified # 👰♀️ E13.0 woman with veil +1F470 200D 2640 ; minimally-qualified # 👰♀ E13.0 woman with veil +1F470 1F3FB 200D 2640 FE0F ; fully-qualified # 👰🏻♀️ E13.0 woman with veil: light skin tone +1F470 1F3FB 200D 2640 ; minimally-qualified # 👰🏻♀ E13.0 woman with veil: light skin tone +1F470 1F3FC 200D 2640 FE0F ; fully-qualified # 👰🏼♀️ E13.0 woman with veil: medium-light skin tone +1F470 1F3FC 200D 2640 ; minimally-qualified # 👰🏼♀ E13.0 woman with veil: medium-light skin tone +1F470 1F3FD 200D 2640 FE0F ; fully-qualified # 👰🏽♀️ E13.0 woman with veil: medium skin tone +1F470 1F3FD 200D 2640 ; minimally-qualified # 👰🏽♀ E13.0 woman with veil: medium skin tone +1F470 1F3FE 200D 2640 FE0F ; fully-qualified # 👰🏾♀️ E13.0 woman with veil: medium-dark skin tone +1F470 1F3FE 200D 2640 ; minimally-qualified # 👰🏾♀ E13.0 woman with veil: medium-dark skin tone +1F470 1F3FF 200D 2640 FE0F ; fully-qualified # 👰🏿♀️ E13.0 woman with veil: dark skin tone +1F470 1F3FF 200D 2640 ; minimally-qualified # 👰🏿♀ E13.0 woman with veil: dark skin tone +1F930 ; fully-qualified # 🤰 E3.0 pregnant woman +1F930 1F3FB ; fully-qualified # 🤰🏻 E3.0 pregnant woman: light skin tone +1F930 1F3FC ; fully-qualified # 🤰🏼 E3.0 pregnant woman: medium-light skin tone +1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone +1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone +1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone +1F931 ; fully-qualified # 🤱 E5.0 breast-feeding +1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone +1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone +1F931 1F3FD ; fully-qualified # 🤱🏽 E5.0 breast-feeding: medium skin tone +1F931 1F3FE ; fully-qualified # 🤱🏾 E5.0 breast-feeding: medium-dark skin tone +1F931 1F3FF ; fully-qualified # 🤱🏿 E5.0 breast-feeding: dark skin tone +1F469 200D 1F37C ; fully-qualified # 👩🍼 E13.0 woman feeding baby +1F469 1F3FB 200D 1F37C ; fully-qualified # 👩🏻🍼 E13.0 woman feeding baby: light skin tone +1F469 1F3FC 200D 1F37C ; fully-qualified # 👩🏼🍼 E13.0 woman feeding baby: medium-light skin tone +1F469 1F3FD 200D 1F37C ; fully-qualified # 👩🏽🍼 E13.0 woman feeding baby: medium skin tone +1F469 1F3FE 200D 1F37C ; fully-qualified # 👩🏾🍼 E13.0 woman feeding baby: medium-dark skin tone +1F469 1F3FF 200D 1F37C ; fully-qualified # 👩🏿🍼 E13.0 woman feeding baby: dark skin tone +1F468 200D 1F37C ; fully-qualified # 👨🍼 E13.0 man feeding baby +1F468 1F3FB 200D 1F37C ; fully-qualified # 👨🏻🍼 E13.0 man feeding baby: light skin tone +1F468 1F3FC 200D 1F37C ; fully-qualified # 👨🏼🍼 E13.0 man feeding baby: medium-light skin tone +1F468 1F3FD 200D 1F37C ; fully-qualified # 👨🏽🍼 E13.0 man feeding baby: medium skin tone +1F468 1F3FE 200D 1F37C ; fully-qualified # 👨🏾🍼 E13.0 man feeding baby: medium-dark skin tone +1F468 1F3FF 200D 1F37C ; fully-qualified # 👨🏿🍼 E13.0 man feeding baby: dark skin tone +1F9D1 200D 1F37C ; fully-qualified # 🧑🍼 E13.0 person feeding baby +1F9D1 1F3FB 200D 1F37C ; fully-qualified # 🧑🏻🍼 E13.0 person feeding baby: light skin tone +1F9D1 1F3FC 200D 1F37C ; fully-qualified # 🧑🏼🍼 E13.0 person feeding baby: medium-light skin tone +1F9D1 1F3FD 200D 1F37C ; fully-qualified # 🧑🏽🍼 E13.0 person feeding baby: medium skin tone +1F9D1 1F3FE 200D 1F37C ; fully-qualified # 🧑🏾🍼 E13.0 person feeding baby: medium-dark skin tone +1F9D1 1F3FF 200D 1F37C ; fully-qualified # 🧑🏿🍼 E13.0 person feeding baby: dark skin tone + +# subgroup: person-fantasy +1F47C ; fully-qualified # 👼 E0.6 baby angel +1F47C 1F3FB ; fully-qualified # 👼🏻 E1.0 baby angel: light skin tone +1F47C 1F3FC ; fully-qualified # 👼🏼 E1.0 baby angel: medium-light skin tone +1F47C 1F3FD ; fully-qualified # 👼🏽 E1.0 baby angel: medium skin tone +1F47C 1F3FE ; fully-qualified # 👼🏾 E1.0 baby angel: medium-dark skin tone +1F47C 1F3FF ; fully-qualified # 👼🏿 E1.0 baby angel: dark skin tone +1F385 ; fully-qualified # 🎅 E0.6 Santa Claus +1F385 1F3FB ; fully-qualified # 🎅🏻 E1.0 Santa Claus: light skin tone +1F385 1F3FC ; fully-qualified # 🎅🏼 E1.0 Santa Claus: medium-light skin tone +1F385 1F3FD ; fully-qualified # 🎅🏽 E1.0 Santa Claus: medium skin tone +1F385 1F3FE ; fully-qualified # 🎅🏾 E1.0 Santa Claus: medium-dark skin tone +1F385 1F3FF ; fully-qualified # 🎅🏿 E1.0 Santa Claus: dark skin tone +1F936 ; fully-qualified # 🤶 E3.0 Mrs. Claus +1F936 1F3FB ; fully-qualified # 🤶🏻 E3.0 Mrs. Claus: light skin tone +1F936 1F3FC ; fully-qualified # 🤶🏼 E3.0 Mrs. Claus: medium-light skin tone +1F936 1F3FD ; fully-qualified # 🤶🏽 E3.0 Mrs. Claus: medium skin tone +1F936 1F3FE ; fully-qualified # 🤶🏾 E3.0 Mrs. Claus: medium-dark skin tone +1F936 1F3FF ; fully-qualified # 🤶🏿 E3.0 Mrs. Claus: dark skin tone +1F9D1 200D 1F384 ; fully-qualified # 🧑🎄 E13.0 mx claus +1F9D1 1F3FB 200D 1F384 ; fully-qualified # 🧑🏻🎄 E13.0 mx claus: light skin tone +1F9D1 1F3FC 200D 1F384 ; fully-qualified # 🧑🏼🎄 E13.0 mx claus: medium-light skin tone +1F9D1 1F3FD 200D 1F384 ; fully-qualified # 🧑🏽🎄 E13.0 mx claus: medium skin tone +1F9D1 1F3FE 200D 1F384 ; fully-qualified # 🧑🏾🎄 E13.0 mx claus: medium-dark skin tone +1F9D1 1F3FF 200D 1F384 ; fully-qualified # 🧑🏿🎄 E13.0 mx claus: dark skin tone +1F9B8 ; fully-qualified # 🦸 E11.0 superhero +1F9B8 1F3FB ; fully-qualified # 🦸🏻 E11.0 superhero: light skin tone +1F9B8 1F3FC ; fully-qualified # 🦸🏼 E11.0 superhero: medium-light skin tone +1F9B8 1F3FD ; fully-qualified # 🦸🏽 E11.0 superhero: medium skin tone +1F9B8 1F3FE ; fully-qualified # 🦸🏾 E11.0 superhero: medium-dark skin tone +1F9B8 1F3FF ; fully-qualified # 🦸🏿 E11.0 superhero: dark skin tone +1F9B8 200D 2642 FE0F ; fully-qualified # 🦸♂️ E11.0 man superhero +1F9B8 200D 2642 ; minimally-qualified # 🦸♂ E11.0 man superhero +1F9B8 1F3FB 200D 2642 FE0F ; fully-qualified # 🦸🏻♂️ E11.0 man superhero: light skin tone +1F9B8 1F3FB 200D 2642 ; minimally-qualified # 🦸🏻♂ E11.0 man superhero: light skin tone +1F9B8 1F3FC 200D 2642 FE0F ; fully-qualified # 🦸🏼♂️ E11.0 man superhero: medium-light skin tone +1F9B8 1F3FC 200D 2642 ; minimally-qualified # 🦸🏼♂ E11.0 man superhero: medium-light skin tone +1F9B8 1F3FD 200D 2642 FE0F ; fully-qualified # 🦸🏽♂️ E11.0 man superhero: medium skin tone +1F9B8 1F3FD 200D 2642 ; minimally-qualified # 🦸🏽♂ E11.0 man superhero: medium skin tone +1F9B8 1F3FE 200D 2642 FE0F ; fully-qualified # 🦸🏾♂️ E11.0 man superhero: medium-dark skin tone +1F9B8 1F3FE 200D 2642 ; minimally-qualified # 🦸🏾♂ E11.0 man superhero: medium-dark skin tone +1F9B8 1F3FF 200D 2642 FE0F ; fully-qualified # 🦸🏿♂️ E11.0 man superhero: dark skin tone +1F9B8 1F3FF 200D 2642 ; minimally-qualified # 🦸🏿♂ E11.0 man superhero: dark skin tone +1F9B8 200D 2640 FE0F ; fully-qualified # 🦸♀️ E11.0 woman superhero +1F9B8 200D 2640 ; minimally-qualified # 🦸♀ E11.0 woman superhero +1F9B8 1F3FB 200D 2640 FE0F ; fully-qualified # 🦸🏻♀️ E11.0 woman superhero: light skin tone +1F9B8 1F3FB 200D 2640 ; minimally-qualified # 🦸🏻♀ E11.0 woman superhero: light skin tone +1F9B8 1F3FC 200D 2640 FE0F ; fully-qualified # 🦸🏼♀️ E11.0 woman superhero: medium-light skin tone +1F9B8 1F3FC 200D 2640 ; minimally-qualified # 🦸🏼♀ E11.0 woman superhero: medium-light skin tone +1F9B8 1F3FD 200D 2640 FE0F ; fully-qualified # 🦸🏽♀️ E11.0 woman superhero: medium skin tone +1F9B8 1F3FD 200D 2640 ; minimally-qualified # 🦸🏽♀ E11.0 woman superhero: medium skin tone +1F9B8 1F3FE 200D 2640 FE0F ; fully-qualified # 🦸🏾♀️ E11.0 woman superhero: medium-dark skin tone +1F9B8 1F3FE 200D 2640 ; minimally-qualified # 🦸🏾♀ E11.0 woman superhero: medium-dark skin tone +1F9B8 1F3FF 200D 2640 FE0F ; fully-qualified # 🦸🏿♀️ E11.0 woman superhero: dark skin tone +1F9B8 1F3FF 200D 2640 ; minimally-qualified # 🦸🏿♀ E11.0 woman superhero: dark skin tone +1F9B9 ; fully-qualified # 🦹 E11.0 supervillain +1F9B9 1F3FB ; fully-qualified # 🦹🏻 E11.0 supervillain: light skin tone +1F9B9 1F3FC ; fully-qualified # 🦹🏼 E11.0 supervillain: medium-light skin tone +1F9B9 1F3FD ; fully-qualified # 🦹🏽 E11.0 supervillain: medium skin tone +1F9B9 1F3FE ; fully-qualified # 🦹🏾 E11.0 supervillain: medium-dark skin tone +1F9B9 1F3FF ; fully-qualified # 🦹🏿 E11.0 supervillain: dark skin tone +1F9B9 200D 2642 FE0F ; fully-qualified # 🦹♂️ E11.0 man supervillain +1F9B9 200D 2642 ; minimally-qualified # 🦹♂ E11.0 man supervillain +1F9B9 1F3FB 200D 2642 FE0F ; fully-qualified # 🦹🏻♂️ E11.0 man supervillain: light skin tone +1F9B9 1F3FB 200D 2642 ; minimally-qualified # 🦹🏻♂ E11.0 man supervillain: light skin tone +1F9B9 1F3FC 200D 2642 FE0F ; fully-qualified # 🦹🏼♂️ E11.0 man supervillain: medium-light skin tone +1F9B9 1F3FC 200D 2642 ; minimally-qualified # 🦹🏼♂ E11.0 man supervillain: medium-light skin tone +1F9B9 1F3FD 200D 2642 FE0F ; fully-qualified # 🦹🏽♂️ E11.0 man supervillain: medium skin tone +1F9B9 1F3FD 200D 2642 ; minimally-qualified # 🦹🏽♂ E11.0 man supervillain: medium skin tone +1F9B9 1F3FE 200D 2642 FE0F ; fully-qualified # 🦹🏾♂️ E11.0 man supervillain: medium-dark skin tone +1F9B9 1F3FE 200D 2642 ; minimally-qualified # 🦹🏾♂ E11.0 man supervillain: medium-dark skin tone +1F9B9 1F3FF 200D 2642 FE0F ; fully-qualified # 🦹🏿♂️ E11.0 man supervillain: dark skin tone +1F9B9 1F3FF 200D 2642 ; minimally-qualified # 🦹🏿♂ E11.0 man supervillain: dark skin tone +1F9B9 200D 2640 FE0F ; fully-qualified # 🦹♀️ E11.0 woman supervillain +1F9B9 200D 2640 ; minimally-qualified # 🦹♀ E11.0 woman supervillain +1F9B9 1F3FB 200D 2640 FE0F ; fully-qualified # 🦹🏻♀️ E11.0 woman supervillain: light skin tone +1F9B9 1F3FB 200D 2640 ; minimally-qualified # 🦹🏻♀ E11.0 woman supervillain: light skin tone +1F9B9 1F3FC 200D 2640 FE0F ; fully-qualified # 🦹🏼♀️ E11.0 woman supervillain: medium-light skin tone +1F9B9 1F3FC 200D 2640 ; minimally-qualified # 🦹🏼♀ E11.0 woman supervillain: medium-light skin tone +1F9B9 1F3FD 200D 2640 FE0F ; fully-qualified # 🦹🏽♀️ E11.0 woman supervillain: medium skin tone +1F9B9 1F3FD 200D 2640 ; minimally-qualified # 🦹🏽♀ E11.0 woman supervillain: medium skin tone +1F9B9 1F3FE 200D 2640 FE0F ; fully-qualified # 🦹🏾♀️ E11.0 woman supervillain: medium-dark skin tone +1F9B9 1F3FE 200D 2640 ; minimally-qualified # 🦹🏾♀ E11.0 woman supervillain: medium-dark skin tone +1F9B9 1F3FF 200D 2640 FE0F ; fully-qualified # 🦹🏿♀️ E11.0 woman supervillain: dark skin tone +1F9B9 1F3FF 200D 2640 ; minimally-qualified # 🦹🏿♀ E11.0 woman supervillain: dark skin tone +1F9D9 ; fully-qualified # 🧙 E5.0 mage +1F9D9 1F3FB ; fully-qualified # 🧙🏻 E5.0 mage: light skin tone +1F9D9 1F3FC ; fully-qualified # 🧙🏼 E5.0 mage: medium-light skin tone +1F9D9 1F3FD ; fully-qualified # 🧙🏽 E5.0 mage: medium skin tone +1F9D9 1F3FE ; fully-qualified # 🧙🏾 E5.0 mage: medium-dark skin tone +1F9D9 1F3FF ; fully-qualified # 🧙🏿 E5.0 mage: dark skin tone +1F9D9 200D 2642 FE0F ; fully-qualified # 🧙♂️ E5.0 man mage +1F9D9 200D 2642 ; minimally-qualified # 🧙♂ E5.0 man mage +1F9D9 1F3FB 200D 2642 FE0F ; fully-qualified # 🧙🏻♂️ E5.0 man mage: light skin tone +1F9D9 1F3FB 200D 2642 ; minimally-qualified # 🧙🏻♂ E5.0 man mage: light skin tone +1F9D9 1F3FC 200D 2642 FE0F ; fully-qualified # 🧙🏼♂️ E5.0 man mage: medium-light skin tone +1F9D9 1F3FC 200D 2642 ; minimally-qualified # 🧙🏼♂ E5.0 man mage: medium-light skin tone +1F9D9 1F3FD 200D 2642 FE0F ; fully-qualified # 🧙🏽♂️ E5.0 man mage: medium skin tone +1F9D9 1F3FD 200D 2642 ; minimally-qualified # 🧙🏽♂ E5.0 man mage: medium skin tone +1F9D9 1F3FE 200D 2642 FE0F ; fully-qualified # 🧙🏾♂️ E5.0 man mage: medium-dark skin tone +1F9D9 1F3FE 200D 2642 ; minimally-qualified # 🧙🏾♂ E5.0 man mage: medium-dark skin tone +1F9D9 1F3FF 200D 2642 FE0F ; fully-qualified # 🧙🏿♂️ E5.0 man mage: dark skin tone +1F9D9 1F3FF 200D 2642 ; minimally-qualified # 🧙🏿♂ E5.0 man mage: dark skin tone +1F9D9 200D 2640 FE0F ; fully-qualified # 🧙♀️ E5.0 woman mage +1F9D9 200D 2640 ; minimally-qualified # 🧙♀ E5.0 woman mage +1F9D9 1F3FB 200D 2640 FE0F ; fully-qualified # 🧙🏻♀️ E5.0 woman mage: light skin tone +1F9D9 1F3FB 200D 2640 ; minimally-qualified # 🧙🏻♀ E5.0 woman mage: light skin tone +1F9D9 1F3FC 200D 2640 FE0F ; fully-qualified # 🧙🏼♀️ E5.0 woman mage: medium-light skin tone +1F9D9 1F3FC 200D 2640 ; minimally-qualified # 🧙🏼♀ E5.0 woman mage: medium-light skin tone +1F9D9 1F3FD 200D 2640 FE0F ; fully-qualified # 🧙🏽♀️ E5.0 woman mage: medium skin tone +1F9D9 1F3FD 200D 2640 ; minimally-qualified # 🧙🏽♀ E5.0 woman mage: medium skin tone +1F9D9 1F3FE 200D 2640 FE0F ; fully-qualified # 🧙🏾♀️ E5.0 woman mage: medium-dark skin tone +1F9D9 1F3FE 200D 2640 ; minimally-qualified # 🧙🏾♀ E5.0 woman mage: medium-dark skin tone +1F9D9 1F3FF 200D 2640 FE0F ; fully-qualified # 🧙🏿♀️ E5.0 woman mage: dark skin tone +1F9D9 1F3FF 200D 2640 ; minimally-qualified # 🧙🏿♀ E5.0 woman mage: dark skin tone +1F9DA ; fully-qualified # 🧚 E5.0 fairy +1F9DA 1F3FB ; fully-qualified # 🧚🏻 E5.0 fairy: light skin tone +1F9DA 1F3FC ; fully-qualified # 🧚🏼 E5.0 fairy: medium-light skin tone +1F9DA 1F3FD ; fully-qualified # 🧚🏽 E5.0 fairy: medium skin tone +1F9DA 1F3FE ; fully-qualified # 🧚🏾 E5.0 fairy: medium-dark skin tone +1F9DA 1F3FF ; fully-qualified # 🧚🏿 E5.0 fairy: dark skin tone +1F9DA 200D 2642 FE0F ; fully-qualified # 🧚♂️ E5.0 man fairy +1F9DA 200D 2642 ; minimally-qualified # 🧚♂ E5.0 man fairy +1F9DA 1F3FB 200D 2642 FE0F ; fully-qualified # 🧚🏻♂️ E5.0 man fairy: light skin tone +1F9DA 1F3FB 200D 2642 ; minimally-qualified # 🧚🏻♂ E5.0 man fairy: light skin tone +1F9DA 1F3FC 200D 2642 FE0F ; fully-qualified # 🧚🏼♂️ E5.0 man fairy: medium-light skin tone +1F9DA 1F3FC 200D 2642 ; minimally-qualified # 🧚🏼♂ E5.0 man fairy: medium-light skin tone +1F9DA 1F3FD 200D 2642 FE0F ; fully-qualified # 🧚🏽♂️ E5.0 man fairy: medium skin tone +1F9DA 1F3FD 200D 2642 ; minimally-qualified # 🧚🏽♂ E5.0 man fairy: medium skin tone +1F9DA 1F3FE 200D 2642 FE0F ; fully-qualified # 🧚🏾♂️ E5.0 man fairy: medium-dark skin tone +1F9DA 1F3FE 200D 2642 ; minimally-qualified # 🧚🏾♂ E5.0 man fairy: medium-dark skin tone +1F9DA 1F3FF 200D 2642 FE0F ; fully-qualified # 🧚🏿♂️ E5.0 man fairy: dark skin tone +1F9DA 1F3FF 200D 2642 ; minimally-qualified # 🧚🏿♂ E5.0 man fairy: dark skin tone +1F9DA 200D 2640 FE0F ; fully-qualified # 🧚♀️ E5.0 woman fairy +1F9DA 200D 2640 ; minimally-qualified # 🧚♀ E5.0 woman fairy +1F9DA 1F3FB 200D 2640 FE0F ; fully-qualified # 🧚🏻♀️ E5.0 woman fairy: light skin tone +1F9DA 1F3FB 200D 2640 ; minimally-qualified # 🧚🏻♀ E5.0 woman fairy: light skin tone +1F9DA 1F3FC 200D 2640 FE0F ; fully-qualified # 🧚🏼♀️ E5.0 woman fairy: medium-light skin tone +1F9DA 1F3FC 200D 2640 ; minimally-qualified # 🧚🏼♀ E5.0 woman fairy: medium-light skin tone +1F9DA 1F3FD 200D 2640 FE0F ; fully-qualified # 🧚🏽♀️ E5.0 woman fairy: medium skin tone +1F9DA 1F3FD 200D 2640 ; minimally-qualified # 🧚🏽♀ E5.0 woman fairy: medium skin tone +1F9DA 1F3FE 200D 2640 FE0F ; fully-qualified # 🧚🏾♀️ E5.0 woman fairy: medium-dark skin tone +1F9DA 1F3FE 200D 2640 ; minimally-qualified # 🧚🏾♀ E5.0 woman fairy: medium-dark skin tone +1F9DA 1F3FF 200D 2640 FE0F ; fully-qualified # 🧚🏿♀️ E5.0 woman fairy: dark skin tone +1F9DA 1F3FF 200D 2640 ; minimally-qualified # 🧚🏿♀ E5.0 woman fairy: dark skin tone +1F9DB ; fully-qualified # 🧛 E5.0 vampire +1F9DB 1F3FB ; fully-qualified # 🧛🏻 E5.0 vampire: light skin tone +1F9DB 1F3FC ; fully-qualified # 🧛🏼 E5.0 vampire: medium-light skin tone +1F9DB 1F3FD ; fully-qualified # 🧛🏽 E5.0 vampire: medium skin tone +1F9DB 1F3FE ; fully-qualified # 🧛🏾 E5.0 vampire: medium-dark skin tone +1F9DB 1F3FF ; fully-qualified # 🧛🏿 E5.0 vampire: dark skin tone +1F9DB 200D 2642 FE0F ; fully-qualified # 🧛♂️ E5.0 man vampire +1F9DB 200D 2642 ; minimally-qualified # 🧛♂ E5.0 man vampire +1F9DB 1F3FB 200D 2642 FE0F ; fully-qualified # 🧛🏻♂️ E5.0 man vampire: light skin tone +1F9DB 1F3FB 200D 2642 ; minimally-qualified # 🧛🏻♂ E5.0 man vampire: light skin tone +1F9DB 1F3FC 200D 2642 FE0F ; fully-qualified # 🧛🏼♂️ E5.0 man vampire: medium-light skin tone +1F9DB 1F3FC 200D 2642 ; minimally-qualified # 🧛🏼♂ E5.0 man vampire: medium-light skin tone +1F9DB 1F3FD 200D 2642 FE0F ; fully-qualified # 🧛🏽♂️ E5.0 man vampire: medium skin tone +1F9DB 1F3FD 200D 2642 ; minimally-qualified # 🧛🏽♂ E5.0 man vampire: medium skin tone +1F9DB 1F3FE 200D 2642 FE0F ; fully-qualified # 🧛🏾♂️ E5.0 man vampire: medium-dark skin tone +1F9DB 1F3FE 200D 2642 ; minimally-qualified # 🧛🏾♂ E5.0 man vampire: medium-dark skin tone +1F9DB 1F3FF 200D 2642 FE0F ; fully-qualified # 🧛🏿♂️ E5.0 man vampire: dark skin tone +1F9DB 1F3FF 200D 2642 ; minimally-qualified # 🧛🏿♂ E5.0 man vampire: dark skin tone +1F9DB 200D 2640 FE0F ; fully-qualified # 🧛♀️ E5.0 woman vampire +1F9DB 200D 2640 ; minimally-qualified # 🧛♀ E5.0 woman vampire +1F9DB 1F3FB 200D 2640 FE0F ; fully-qualified # 🧛🏻♀️ E5.0 woman vampire: light skin tone +1F9DB 1F3FB 200D 2640 ; minimally-qualified # 🧛🏻♀ E5.0 woman vampire: light skin tone +1F9DB 1F3FC 200D 2640 FE0F ; fully-qualified # 🧛🏼♀️ E5.0 woman vampire: medium-light skin tone +1F9DB 1F3FC 200D 2640 ; minimally-qualified # 🧛🏼♀ E5.0 woman vampire: medium-light skin tone +1F9DB 1F3FD 200D 2640 FE0F ; fully-qualified # 🧛🏽♀️ E5.0 woman vampire: medium skin tone +1F9DB 1F3FD 200D 2640 ; minimally-qualified # 🧛🏽♀ E5.0 woman vampire: medium skin tone +1F9DB 1F3FE 200D 2640 FE0F ; fully-qualified # 🧛🏾♀️ E5.0 woman vampire: medium-dark skin tone +1F9DB 1F3FE 200D 2640 ; minimally-qualified # 🧛🏾♀ E5.0 woman vampire: medium-dark skin tone +1F9DB 1F3FF 200D 2640 FE0F ; fully-qualified # 🧛🏿♀️ E5.0 woman vampire: dark skin tone +1F9DB 1F3FF 200D 2640 ; minimally-qualified # 🧛🏿♀ E5.0 woman vampire: dark skin tone +1F9DC ; fully-qualified # 🧜 E5.0 merperson +1F9DC 1F3FB ; fully-qualified # 🧜🏻 E5.0 merperson: light skin tone +1F9DC 1F3FC ; fully-qualified # 🧜🏼 E5.0 merperson: medium-light skin tone +1F9DC 1F3FD ; fully-qualified # 🧜🏽 E5.0 merperson: medium skin tone +1F9DC 1F3FE ; fully-qualified # 🧜🏾 E5.0 merperson: medium-dark skin tone +1F9DC 1F3FF ; fully-qualified # 🧜🏿 E5.0 merperson: dark skin tone +1F9DC 200D 2642 FE0F ; fully-qualified # 🧜♂️ E5.0 merman +1F9DC 200D 2642 ; minimally-qualified # 🧜♂ E5.0 merman +1F9DC 1F3FB 200D 2642 FE0F ; fully-qualified # 🧜🏻♂️ E5.0 merman: light skin tone +1F9DC 1F3FB 200D 2642 ; minimally-qualified # 🧜🏻♂ E5.0 merman: light skin tone +1F9DC 1F3FC 200D 2642 FE0F ; fully-qualified # 🧜🏼♂️ E5.0 merman: medium-light skin tone +1F9DC 1F3FC 200D 2642 ; minimally-qualified # 🧜🏼♂ E5.0 merman: medium-light skin tone +1F9DC 1F3FD 200D 2642 FE0F ; fully-qualified # 🧜🏽♂️ E5.0 merman: medium skin tone +1F9DC 1F3FD 200D 2642 ; minimally-qualified # 🧜🏽♂ E5.0 merman: medium skin tone +1F9DC 1F3FE 200D 2642 FE0F ; fully-qualified # 🧜🏾♂️ E5.0 merman: medium-dark skin tone +1F9DC 1F3FE 200D 2642 ; minimally-qualified # 🧜🏾♂ E5.0 merman: medium-dark skin tone +1F9DC 1F3FF 200D 2642 FE0F ; fully-qualified # 🧜🏿♂️ E5.0 merman: dark skin tone +1F9DC 1F3FF 200D 2642 ; minimally-qualified # 🧜🏿♂ E5.0 merman: dark skin tone +1F9DC 200D 2640 FE0F ; fully-qualified # 🧜♀️ E5.0 mermaid +1F9DC 200D 2640 ; minimally-qualified # 🧜♀ E5.0 mermaid +1F9DC 1F3FB 200D 2640 FE0F ; fully-qualified # 🧜🏻♀️ E5.0 mermaid: light skin tone +1F9DC 1F3FB 200D 2640 ; minimally-qualified # 🧜🏻♀ E5.0 mermaid: light skin tone +1F9DC 1F3FC 200D 2640 FE0F ; fully-qualified # 🧜🏼♀️ E5.0 mermaid: medium-light skin tone +1F9DC 1F3FC 200D 2640 ; minimally-qualified # 🧜🏼♀ E5.0 mermaid: medium-light skin tone +1F9DC 1F3FD 200D 2640 FE0F ; fully-qualified # 🧜🏽♀️ E5.0 mermaid: medium skin tone +1F9DC 1F3FD 200D 2640 ; minimally-qualified # 🧜🏽♀ E5.0 mermaid: medium skin tone +1F9DC 1F3FE 200D 2640 FE0F ; fully-qualified # 🧜🏾♀️ E5.0 mermaid: medium-dark skin tone +1F9DC 1F3FE 200D 2640 ; minimally-qualified # 🧜🏾♀ E5.0 mermaid: medium-dark skin tone +1F9DC 1F3FF 200D 2640 FE0F ; fully-qualified # 🧜🏿♀️ E5.0 mermaid: dark skin tone +1F9DC 1F3FF 200D 2640 ; minimally-qualified # 🧜🏿♀ E5.0 mermaid: dark skin tone +1F9DD ; fully-qualified # 🧝 E5.0 elf +1F9DD 1F3FB ; fully-qualified # 🧝🏻 E5.0 elf: light skin tone +1F9DD 1F3FC ; fully-qualified # 🧝🏼 E5.0 elf: medium-light skin tone +1F9DD 1F3FD ; fully-qualified # 🧝🏽 E5.0 elf: medium skin tone +1F9DD 1F3FE ; fully-qualified # 🧝🏾 E5.0 elf: medium-dark skin tone +1F9DD 1F3FF ; fully-qualified # 🧝🏿 E5.0 elf: dark skin tone +1F9DD 200D 2642 FE0F ; fully-qualified # 🧝♂️ E5.0 man elf +1F9DD 200D 2642 ; minimally-qualified # 🧝♂ E5.0 man elf +1F9DD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧝🏻♂️ E5.0 man elf: light skin tone +1F9DD 1F3FB 200D 2642 ; minimally-qualified # 🧝🏻♂ E5.0 man elf: light skin tone +1F9DD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧝🏼♂️ E5.0 man elf: medium-light skin tone +1F9DD 1F3FC 200D 2642 ; minimally-qualified # 🧝🏼♂ E5.0 man elf: medium-light skin tone +1F9DD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧝🏽♂️ E5.0 man elf: medium skin tone +1F9DD 1F3FD 200D 2642 ; minimally-qualified # 🧝🏽♂ E5.0 man elf: medium skin tone +1F9DD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧝🏾♂️ E5.0 man elf: medium-dark skin tone +1F9DD 1F3FE 200D 2642 ; minimally-qualified # 🧝🏾♂ E5.0 man elf: medium-dark skin tone +1F9DD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧝🏿♂️ E5.0 man elf: dark skin tone +1F9DD 1F3FF 200D 2642 ; minimally-qualified # 🧝🏿♂ E5.0 man elf: dark skin tone +1F9DD 200D 2640 FE0F ; fully-qualified # 🧝♀️ E5.0 woman elf +1F9DD 200D 2640 ; minimally-qualified # 🧝♀ E5.0 woman elf +1F9DD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧝🏻♀️ E5.0 woman elf: light skin tone +1F9DD 1F3FB 200D 2640 ; minimally-qualified # 🧝🏻♀ E5.0 woman elf: light skin tone +1F9DD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧝🏼♀️ E5.0 woman elf: medium-light skin tone +1F9DD 1F3FC 200D 2640 ; minimally-qualified # 🧝🏼♀ E5.0 woman elf: medium-light skin tone +1F9DD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧝🏽♀️ E5.0 woman elf: medium skin tone +1F9DD 1F3FD 200D 2640 ; minimally-qualified # 🧝🏽♀ E5.0 woman elf: medium skin tone +1F9DD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧝🏾♀️ E5.0 woman elf: medium-dark skin tone +1F9DD 1F3FE 200D 2640 ; minimally-qualified # 🧝🏾♀ E5.0 woman elf: medium-dark skin tone +1F9DD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧝🏿♀️ E5.0 woman elf: dark skin tone +1F9DD 1F3FF 200D 2640 ; minimally-qualified # 🧝🏿♀ E5.0 woman elf: dark skin tone +1F9DE ; fully-qualified # 🧞 E5.0 genie +1F9DE 200D 2642 FE0F ; fully-qualified # 🧞♂️ E5.0 man genie +1F9DE 200D 2642 ; minimally-qualified # 🧞♂ E5.0 man genie +1F9DE 200D 2640 FE0F ; fully-qualified # 🧞♀️ E5.0 woman genie +1F9DE 200D 2640 ; minimally-qualified # 🧞♀ E5.0 woman genie +1F9DF ; fully-qualified # 🧟 E5.0 zombie +1F9DF 200D 2642 FE0F ; fully-qualified # 🧟♂️ E5.0 man zombie +1F9DF 200D 2642 ; minimally-qualified # 🧟♂ E5.0 man zombie +1F9DF 200D 2640 FE0F ; fully-qualified # 🧟♀️ E5.0 woman zombie +1F9DF 200D 2640 ; minimally-qualified # 🧟♀ E5.0 woman zombie + +# subgroup: person-activity +1F486 ; fully-qualified # 💆 E0.6 person getting massage +1F486 1F3FB ; fully-qualified # 💆🏻 E1.0 person getting massage: light skin tone +1F486 1F3FC ; fully-qualified # 💆🏼 E1.0 person getting massage: medium-light skin tone +1F486 1F3FD ; fully-qualified # 💆🏽 E1.0 person getting massage: medium skin tone +1F486 1F3FE ; fully-qualified # 💆🏾 E1.0 person getting massage: medium-dark skin tone +1F486 1F3FF ; fully-qualified # 💆🏿 E1.0 person getting massage: dark skin tone +1F486 200D 2642 FE0F ; fully-qualified # 💆♂️ E4.0 man getting massage +1F486 200D 2642 ; minimally-qualified # 💆♂ E4.0 man getting massage +1F486 1F3FB 200D 2642 FE0F ; fully-qualified # 💆🏻♂️ E4.0 man getting massage: light skin tone +1F486 1F3FB 200D 2642 ; minimally-qualified # 💆🏻♂ E4.0 man getting massage: light skin tone +1F486 1F3FC 200D 2642 FE0F ; fully-qualified # 💆🏼♂️ E4.0 man getting massage: medium-light skin tone +1F486 1F3FC 200D 2642 ; minimally-qualified # 💆🏼♂ E4.0 man getting massage: medium-light skin tone +1F486 1F3FD 200D 2642 FE0F ; fully-qualified # 💆🏽♂️ E4.0 man getting massage: medium skin tone +1F486 1F3FD 200D 2642 ; minimally-qualified # 💆🏽♂ E4.0 man getting massage: medium skin tone +1F486 1F3FE 200D 2642 FE0F ; fully-qualified # 💆🏾♂️ E4.0 man getting massage: medium-dark skin tone +1F486 1F3FE 200D 2642 ; minimally-qualified # 💆🏾♂ E4.0 man getting massage: medium-dark skin tone +1F486 1F3FF 200D 2642 FE0F ; fully-qualified # 💆🏿♂️ E4.0 man getting massage: dark skin tone +1F486 1F3FF 200D 2642 ; minimally-qualified # 💆🏿♂ E4.0 man getting massage: dark skin tone +1F486 200D 2640 FE0F ; fully-qualified # 💆♀️ E4.0 woman getting massage +1F486 200D 2640 ; minimally-qualified # 💆♀ E4.0 woman getting massage +1F486 1F3FB 200D 2640 FE0F ; fully-qualified # 💆🏻♀️ E4.0 woman getting massage: light skin tone +1F486 1F3FB 200D 2640 ; minimally-qualified # 💆🏻♀ E4.0 woman getting massage: light skin tone +1F486 1F3FC 200D 2640 FE0F ; fully-qualified # 💆🏼♀️ E4.0 woman getting massage: medium-light skin tone +1F486 1F3FC 200D 2640 ; minimally-qualified # 💆🏼♀ E4.0 woman getting massage: medium-light skin tone +1F486 1F3FD 200D 2640 FE0F ; fully-qualified # 💆🏽♀️ E4.0 woman getting massage: medium skin tone +1F486 1F3FD 200D 2640 ; minimally-qualified # 💆🏽♀ E4.0 woman getting massage: medium skin tone +1F486 1F3FE 200D 2640 FE0F ; fully-qualified # 💆🏾♀️ E4.0 woman getting massage: medium-dark skin tone +1F486 1F3FE 200D 2640 ; minimally-qualified # 💆🏾♀ E4.0 woman getting massage: medium-dark skin tone +1F486 1F3FF 200D 2640 FE0F ; fully-qualified # 💆🏿♀️ E4.0 woman getting massage: dark skin tone +1F486 1F3FF 200D 2640 ; minimally-qualified # 💆🏿♀ E4.0 woman getting massage: dark skin tone +1F487 ; fully-qualified # 💇 E0.6 person getting haircut +1F487 1F3FB ; fully-qualified # 💇🏻 E1.0 person getting haircut: light skin tone +1F487 1F3FC ; fully-qualified # 💇🏼 E1.0 person getting haircut: medium-light skin tone +1F487 1F3FD ; fully-qualified # 💇🏽 E1.0 person getting haircut: medium skin tone +1F487 1F3FE ; fully-qualified # 💇🏾 E1.0 person getting haircut: medium-dark skin tone +1F487 1F3FF ; fully-qualified # 💇🏿 E1.0 person getting haircut: dark skin tone +1F487 200D 2642 FE0F ; fully-qualified # 💇♂️ E4.0 man getting haircut +1F487 200D 2642 ; minimally-qualified # 💇♂ E4.0 man getting haircut +1F487 1F3FB 200D 2642 FE0F ; fully-qualified # 💇🏻♂️ E4.0 man getting haircut: light skin tone +1F487 1F3FB 200D 2642 ; minimally-qualified # 💇🏻♂ E4.0 man getting haircut: light skin tone +1F487 1F3FC 200D 2642 FE0F ; fully-qualified # 💇🏼♂️ E4.0 man getting haircut: medium-light skin tone +1F487 1F3FC 200D 2642 ; minimally-qualified # 💇🏼♂ E4.0 man getting haircut: medium-light skin tone +1F487 1F3FD 200D 2642 FE0F ; fully-qualified # 💇🏽♂️ E4.0 man getting haircut: medium skin tone +1F487 1F3FD 200D 2642 ; minimally-qualified # 💇🏽♂ E4.0 man getting haircut: medium skin tone +1F487 1F3FE 200D 2642 FE0F ; fully-qualified # 💇🏾♂️ E4.0 man getting haircut: medium-dark skin tone +1F487 1F3FE 200D 2642 ; minimally-qualified # 💇🏾♂ E4.0 man getting haircut: medium-dark skin tone +1F487 1F3FF 200D 2642 FE0F ; fully-qualified # 💇🏿♂️ E4.0 man getting haircut: dark skin tone +1F487 1F3FF 200D 2642 ; minimally-qualified # 💇🏿♂ E4.0 man getting haircut: dark skin tone +1F487 200D 2640 FE0F ; fully-qualified # 💇♀️ E4.0 woman getting haircut +1F487 200D 2640 ; minimally-qualified # 💇♀ E4.0 woman getting haircut +1F487 1F3FB 200D 2640 FE0F ; fully-qualified # 💇🏻♀️ E4.0 woman getting haircut: light skin tone +1F487 1F3FB 200D 2640 ; minimally-qualified # 💇🏻♀ E4.0 woman getting haircut: light skin tone +1F487 1F3FC 200D 2640 FE0F ; fully-qualified # 💇🏼♀️ E4.0 woman getting haircut: medium-light skin tone +1F487 1F3FC 200D 2640 ; minimally-qualified # 💇🏼♀ E4.0 woman getting haircut: medium-light skin tone +1F487 1F3FD 200D 2640 FE0F ; fully-qualified # 💇🏽♀️ E4.0 woman getting haircut: medium skin tone +1F487 1F3FD 200D 2640 ; minimally-qualified # 💇🏽♀ E4.0 woman getting haircut: medium skin tone +1F487 1F3FE 200D 2640 FE0F ; fully-qualified # 💇🏾♀️ E4.0 woman getting haircut: medium-dark skin tone +1F487 1F3FE 200D 2640 ; minimally-qualified # 💇🏾♀ E4.0 woman getting haircut: medium-dark skin tone +1F487 1F3FF 200D 2640 FE0F ; fully-qualified # 💇🏿♀️ E4.0 woman getting haircut: dark skin tone +1F487 1F3FF 200D 2640 ; minimally-qualified # 💇🏿♀ E4.0 woman getting haircut: dark skin tone +1F6B6 ; fully-qualified # 🚶 E0.6 person walking +1F6B6 1F3FB ; fully-qualified # 🚶🏻 E1.0 person walking: light skin tone +1F6B6 1F3FC ; fully-qualified # 🚶🏼 E1.0 person walking: medium-light skin tone +1F6B6 1F3FD ; fully-qualified # 🚶🏽 E1.0 person walking: medium skin tone +1F6B6 1F3FE ; fully-qualified # 🚶🏾 E1.0 person walking: medium-dark skin tone +1F6B6 1F3FF ; fully-qualified # 🚶🏿 E1.0 person walking: dark skin tone +1F6B6 200D 2642 FE0F ; fully-qualified # 🚶♂️ E4.0 man walking +1F6B6 200D 2642 ; minimally-qualified # 🚶♂ E4.0 man walking +1F6B6 1F3FB 200D 2642 FE0F ; fully-qualified # 🚶🏻♂️ E4.0 man walking: light skin tone +1F6B6 1F3FB 200D 2642 ; minimally-qualified # 🚶🏻♂ E4.0 man walking: light skin tone +1F6B6 1F3FC 200D 2642 FE0F ; fully-qualified # 🚶🏼♂️ E4.0 man walking: medium-light skin tone +1F6B6 1F3FC 200D 2642 ; minimally-qualified # 🚶🏼♂ E4.0 man walking: medium-light skin tone +1F6B6 1F3FD 200D 2642 FE0F ; fully-qualified # 🚶🏽♂️ E4.0 man walking: medium skin tone +1F6B6 1F3FD 200D 2642 ; minimally-qualified # 🚶🏽♂ E4.0 man walking: medium skin tone +1F6B6 1F3FE 200D 2642 FE0F ; fully-qualified # 🚶🏾♂️ E4.0 man walking: medium-dark skin tone +1F6B6 1F3FE 200D 2642 ; minimally-qualified # 🚶🏾♂ E4.0 man walking: medium-dark skin tone +1F6B6 1F3FF 200D 2642 FE0F ; fully-qualified # 🚶🏿♂️ E4.0 man walking: dark skin tone +1F6B6 1F3FF 200D 2642 ; minimally-qualified # 🚶🏿♂ E4.0 man walking: dark skin tone +1F6B6 200D 2640 FE0F ; fully-qualified # 🚶♀️ E4.0 woman walking +1F6B6 200D 2640 ; minimally-qualified # 🚶♀ E4.0 woman walking +1F6B6 1F3FB 200D 2640 FE0F ; fully-qualified # 🚶🏻♀️ E4.0 woman walking: light skin tone +1F6B6 1F3FB 200D 2640 ; minimally-qualified # 🚶🏻♀ E4.0 woman walking: light skin tone +1F6B6 1F3FC 200D 2640 FE0F ; fully-qualified # 🚶🏼♀️ E4.0 woman walking: medium-light skin tone +1F6B6 1F3FC 200D 2640 ; minimally-qualified # 🚶🏼♀ E4.0 woman walking: medium-light skin tone +1F6B6 1F3FD 200D 2640 FE0F ; fully-qualified # 🚶🏽♀️ E4.0 woman walking: medium skin tone +1F6B6 1F3FD 200D 2640 ; minimally-qualified # 🚶🏽♀ E4.0 woman walking: medium skin tone +1F6B6 1F3FE 200D 2640 FE0F ; fully-qualified # 🚶🏾♀️ E4.0 woman walking: medium-dark skin tone +1F6B6 1F3FE 200D 2640 ; minimally-qualified # 🚶🏾♀ E4.0 woman walking: medium-dark skin tone +1F6B6 1F3FF 200D 2640 FE0F ; fully-qualified # 🚶🏿♀️ E4.0 woman walking: dark skin tone +1F6B6 1F3FF 200D 2640 ; minimally-qualified # 🚶🏿♀ E4.0 woman walking: dark skin tone +1F9CD ; fully-qualified # 🧍 E12.0 person standing +1F9CD 1F3FB ; fully-qualified # 🧍🏻 E12.0 person standing: light skin tone +1F9CD 1F3FC ; fully-qualified # 🧍🏼 E12.0 person standing: medium-light skin tone +1F9CD 1F3FD ; fully-qualified # 🧍🏽 E12.0 person standing: medium skin tone +1F9CD 1F3FE ; fully-qualified # 🧍🏾 E12.0 person standing: medium-dark skin tone +1F9CD 1F3FF ; fully-qualified # 🧍🏿 E12.0 person standing: dark skin tone +1F9CD 200D 2642 FE0F ; fully-qualified # 🧍♂️ E12.0 man standing +1F9CD 200D 2642 ; minimally-qualified # 🧍♂ E12.0 man standing +1F9CD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧍🏻♂️ E12.0 man standing: light skin tone +1F9CD 1F3FB 200D 2642 ; minimally-qualified # 🧍🏻♂ E12.0 man standing: light skin tone +1F9CD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧍🏼♂️ E12.0 man standing: medium-light skin tone +1F9CD 1F3FC 200D 2642 ; minimally-qualified # 🧍🏼♂ E12.0 man standing: medium-light skin tone +1F9CD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧍🏽♂️ E12.0 man standing: medium skin tone +1F9CD 1F3FD 200D 2642 ; minimally-qualified # 🧍🏽♂ E12.0 man standing: medium skin tone +1F9CD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧍🏾♂️ E12.0 man standing: medium-dark skin tone +1F9CD 1F3FE 200D 2642 ; minimally-qualified # 🧍🏾♂ E12.0 man standing: medium-dark skin tone +1F9CD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧍🏿♂️ E12.0 man standing: dark skin tone +1F9CD 1F3FF 200D 2642 ; minimally-qualified # 🧍🏿♂ E12.0 man standing: dark skin tone +1F9CD 200D 2640 FE0F ; fully-qualified # 🧍♀️ E12.0 woman standing +1F9CD 200D 2640 ; minimally-qualified # 🧍♀ E12.0 woman standing +1F9CD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧍🏻♀️ E12.0 woman standing: light skin tone +1F9CD 1F3FB 200D 2640 ; minimally-qualified # 🧍🏻♀ E12.0 woman standing: light skin tone +1F9CD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧍🏼♀️ E12.0 woman standing: medium-light skin tone +1F9CD 1F3FC 200D 2640 ; minimally-qualified # 🧍🏼♀ E12.0 woman standing: medium-light skin tone +1F9CD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧍🏽♀️ E12.0 woman standing: medium skin tone +1F9CD 1F3FD 200D 2640 ; minimally-qualified # 🧍🏽♀ E12.0 woman standing: medium skin tone +1F9CD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧍🏾♀️ E12.0 woman standing: medium-dark skin tone +1F9CD 1F3FE 200D 2640 ; minimally-qualified # 🧍🏾♀ E12.0 woman standing: medium-dark skin tone +1F9CD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧍🏿♀️ E12.0 woman standing: dark skin tone +1F9CD 1F3FF 200D 2640 ; minimally-qualified # 🧍🏿♀ E12.0 woman standing: dark skin tone +1F9CE ; fully-qualified # 🧎 E12.0 person kneeling +1F9CE 1F3FB ; fully-qualified # 🧎🏻 E12.0 person kneeling: light skin tone +1F9CE 1F3FC ; fully-qualified # 🧎🏼 E12.0 person kneeling: medium-light skin tone +1F9CE 1F3FD ; fully-qualified # 🧎🏽 E12.0 person kneeling: medium skin tone +1F9CE 1F3FE ; fully-qualified # 🧎🏾 E12.0 person kneeling: medium-dark skin tone +1F9CE 1F3FF ; fully-qualified # 🧎🏿 E12.0 person kneeling: dark skin tone +1F9CE 200D 2642 FE0F ; fully-qualified # 🧎♂️ E12.0 man kneeling +1F9CE 200D 2642 ; minimally-qualified # 🧎♂ E12.0 man kneeling +1F9CE 1F3FB 200D 2642 FE0F ; fully-qualified # 🧎🏻♂️ E12.0 man kneeling: light skin tone +1F9CE 1F3FB 200D 2642 ; minimally-qualified # 🧎🏻♂ E12.0 man kneeling: light skin tone +1F9CE 1F3FC 200D 2642 FE0F ; fully-qualified # 🧎🏼♂️ E12.0 man kneeling: medium-light skin tone +1F9CE 1F3FC 200D 2642 ; minimally-qualified # 🧎🏼♂ E12.0 man kneeling: medium-light skin tone +1F9CE 1F3FD 200D 2642 FE0F ; fully-qualified # 🧎🏽♂️ E12.0 man kneeling: medium skin tone +1F9CE 1F3FD 200D 2642 ; minimally-qualified # 🧎🏽♂ E12.0 man kneeling: medium skin tone +1F9CE 1F3FE 200D 2642 FE0F ; fully-qualified # 🧎🏾♂️ E12.0 man kneeling: medium-dark skin tone +1F9CE 1F3FE 200D 2642 ; minimally-qualified # 🧎🏾♂ E12.0 man kneeling: medium-dark skin tone +1F9CE 1F3FF 200D 2642 FE0F ; fully-qualified # 🧎🏿♂️ E12.0 man kneeling: dark skin tone +1F9CE 1F3FF 200D 2642 ; minimally-qualified # 🧎🏿♂ E12.0 man kneeling: dark skin tone +1F9CE 200D 2640 FE0F ; fully-qualified # 🧎♀️ E12.0 woman kneeling +1F9CE 200D 2640 ; minimally-qualified # 🧎♀ E12.0 woman kneeling +1F9CE 1F3FB 200D 2640 FE0F ; fully-qualified # 🧎🏻♀️ E12.0 woman kneeling: light skin tone +1F9CE 1F3FB 200D 2640 ; minimally-qualified # 🧎🏻♀ E12.0 woman kneeling: light skin tone +1F9CE 1F3FC 200D 2640 FE0F ; fully-qualified # 🧎🏼♀️ E12.0 woman kneeling: medium-light skin tone +1F9CE 1F3FC 200D 2640 ; minimally-qualified # 🧎🏼♀ E12.0 woman kneeling: medium-light skin tone +1F9CE 1F3FD 200D 2640 FE0F ; fully-qualified # 🧎🏽♀️ E12.0 woman kneeling: medium skin tone +1F9CE 1F3FD 200D 2640 ; minimally-qualified # 🧎🏽♀ E12.0 woman kneeling: medium skin tone +1F9CE 1F3FE 200D 2640 FE0F ; fully-qualified # 🧎🏾♀️ E12.0 woman kneeling: medium-dark skin tone +1F9CE 1F3FE 200D 2640 ; minimally-qualified # 🧎🏾♀ E12.0 woman kneeling: medium-dark skin tone +1F9CE 1F3FF 200D 2640 FE0F ; fully-qualified # 🧎🏿♀️ E12.0 woman kneeling: dark skin tone +1F9CE 1F3FF 200D 2640 ; minimally-qualified # 🧎🏿♀ E12.0 woman kneeling: dark skin tone +1F9D1 200D 1F9AF ; fully-qualified # 🧑🦯 E12.1 person with probing cane +1F9D1 1F3FB 200D 1F9AF ; fully-qualified # 🧑🏻🦯 E12.1 person with probing cane: light skin tone +1F9D1 1F3FC 200D 1F9AF ; fully-qualified # 🧑🏼🦯 E12.1 person with probing cane: medium-light skin tone +1F9D1 1F3FD 200D 1F9AF ; fully-qualified # 🧑🏽🦯 E12.1 person with probing cane: medium skin tone +1F9D1 1F3FE 200D 1F9AF ; fully-qualified # 🧑🏾🦯 E12.1 person with probing cane: medium-dark skin tone +1F9D1 1F3FF 200D 1F9AF ; fully-qualified # 🧑🏿🦯 E12.1 person with probing cane: dark skin tone +1F468 200D 1F9AF ; fully-qualified # 👨🦯 E12.0 man with probing cane +1F468 1F3FB 200D 1F9AF ; fully-qualified # 👨🏻🦯 E12.0 man with probing cane: light skin tone +1F468 1F3FC 200D 1F9AF ; fully-qualified # 👨🏼🦯 E12.0 man with probing cane: medium-light skin tone +1F468 1F3FD 200D 1F9AF ; fully-qualified # 👨🏽🦯 E12.0 man with probing cane: medium skin tone +1F468 1F3FE 200D 1F9AF ; fully-qualified # 👨🏾🦯 E12.0 man with probing cane: medium-dark skin tone +1F468 1F3FF 200D 1F9AF ; fully-qualified # 👨🏿🦯 E12.0 man with probing cane: dark skin tone +1F469 200D 1F9AF ; fully-qualified # 👩🦯 E12.0 woman with probing cane +1F469 1F3FB 200D 1F9AF ; fully-qualified # 👩🏻🦯 E12.0 woman with probing cane: light skin tone +1F469 1F3FC 200D 1F9AF ; fully-qualified # 👩🏼🦯 E12.0 woman with probing cane: medium-light skin tone +1F469 1F3FD 200D 1F9AF ; fully-qualified # 👩🏽🦯 E12.0 woman with probing cane: medium skin tone +1F469 1F3FE 200D 1F9AF ; fully-qualified # 👩🏾🦯 E12.0 woman with probing cane: medium-dark skin tone +1F469 1F3FF 200D 1F9AF ; fully-qualified # 👩🏿🦯 E12.0 woman with probing cane: dark skin tone +1F9D1 200D 1F9BC ; fully-qualified # 🧑🦼 E12.1 person in motorized wheelchair +1F9D1 1F3FB 200D 1F9BC ; fully-qualified # 🧑🏻🦼 E12.1 person in motorized wheelchair: light skin tone +1F9D1 1F3FC 200D 1F9BC ; fully-qualified # 🧑🏼🦼 E12.1 person in motorized wheelchair: medium-light skin tone +1F9D1 1F3FD 200D 1F9BC ; fully-qualified # 🧑🏽🦼 E12.1 person in motorized wheelchair: medium skin tone +1F9D1 1F3FE 200D 1F9BC ; fully-qualified # 🧑🏾🦼 E12.1 person in motorized wheelchair: medium-dark skin tone +1F9D1 1F3FF 200D 1F9BC ; fully-qualified # 🧑🏿🦼 E12.1 person in motorized wheelchair: dark skin tone +1F468 200D 1F9BC ; fully-qualified # 👨🦼 E12.0 man in motorized wheelchair +1F468 1F3FB 200D 1F9BC ; fully-qualified # 👨🏻🦼 E12.0 man in motorized wheelchair: light skin tone +1F468 1F3FC 200D 1F9BC ; fully-qualified # 👨🏼🦼 E12.0 man in motorized wheelchair: medium-light skin tone +1F468 1F3FD 200D 1F9BC ; fully-qualified # 👨🏽🦼 E12.0 man in motorized wheelchair: medium skin tone +1F468 1F3FE 200D 1F9BC ; fully-qualified # 👨🏾🦼 E12.0 man in motorized wheelchair: medium-dark skin tone +1F468 1F3FF 200D 1F9BC ; fully-qualified # 👨🏿🦼 E12.0 man in motorized wheelchair: dark skin tone +1F469 200D 1F9BC ; fully-qualified # 👩🦼 E12.0 woman in motorized wheelchair +1F469 1F3FB 200D 1F9BC ; fully-qualified # 👩🏻🦼 E12.0 woman in motorized wheelchair: light skin tone +1F469 1F3FC 200D 1F9BC ; fully-qualified # 👩🏼🦼 E12.0 woman in motorized wheelchair: medium-light skin tone +1F469 1F3FD 200D 1F9BC ; fully-qualified # 👩🏽🦼 E12.0 woman in motorized wheelchair: medium skin tone +1F469 1F3FE 200D 1F9BC ; fully-qualified # 👩🏾🦼 E12.0 woman in motorized wheelchair: medium-dark skin tone +1F469 1F3FF 200D 1F9BC ; fully-qualified # 👩🏿🦼 E12.0 woman in motorized wheelchair: dark skin tone +1F9D1 200D 1F9BD ; fully-qualified # 🧑🦽 E12.1 person in manual wheelchair +1F9D1 1F3FB 200D 1F9BD ; fully-qualified # 🧑🏻🦽 E12.1 person in manual wheelchair: light skin tone +1F9D1 1F3FC 200D 1F9BD ; fully-qualified # 🧑🏼🦽 E12.1 person in manual wheelchair: medium-light skin tone +1F9D1 1F3FD 200D 1F9BD ; fully-qualified # 🧑🏽🦽 E12.1 person in manual wheelchair: medium skin tone +1F9D1 1F3FE 200D 1F9BD ; fully-qualified # 🧑🏾🦽 E12.1 person in manual wheelchair: medium-dark skin tone +1F9D1 1F3FF 200D 1F9BD ; fully-qualified # 🧑🏿🦽 E12.1 person in manual wheelchair: dark skin tone +1F468 200D 1F9BD ; fully-qualified # 👨🦽 E12.0 man in manual wheelchair +1F468 1F3FB 200D 1F9BD ; fully-qualified # 👨🏻🦽 E12.0 man in manual wheelchair: light skin tone +1F468 1F3FC 200D 1F9BD ; fully-qualified # 👨🏼🦽 E12.0 man in manual wheelchair: medium-light skin tone +1F468 1F3FD 200D 1F9BD ; fully-qualified # 👨🏽🦽 E12.0 man in manual wheelchair: medium skin tone +1F468 1F3FE 200D 1F9BD ; fully-qualified # 👨🏾🦽 E12.0 man in manual wheelchair: medium-dark skin tone +1F468 1F3FF 200D 1F9BD ; fully-qualified # 👨🏿🦽 E12.0 man in manual wheelchair: dark skin tone +1F469 200D 1F9BD ; fully-qualified # 👩🦽 E12.0 woman in manual wheelchair +1F469 1F3FB 200D 1F9BD ; fully-qualified # 👩🏻🦽 E12.0 woman in manual wheelchair: light skin tone +1F469 1F3FC 200D 1F9BD ; fully-qualified # 👩🏼🦽 E12.0 woman in manual wheelchair: medium-light skin tone +1F469 1F3FD 200D 1F9BD ; fully-qualified # 👩🏽🦽 E12.0 woman in manual wheelchair: medium skin tone +1F469 1F3FE 200D 1F9BD ; fully-qualified # 👩🏾🦽 E12.0 woman in manual wheelchair: medium-dark skin tone +1F469 1F3FF 200D 1F9BD ; fully-qualified # 👩🏿🦽 E12.0 woman in manual wheelchair: dark skin tone +1F3C3 ; fully-qualified # 🏃 E0.6 person running +1F3C3 1F3FB ; fully-qualified # 🏃🏻 E1.0 person running: light skin tone +1F3C3 1F3FC ; fully-qualified # 🏃🏼 E1.0 person running: medium-light skin tone +1F3C3 1F3FD ; fully-qualified # 🏃🏽 E1.0 person running: medium skin tone +1F3C3 1F3FE ; fully-qualified # 🏃🏾 E1.0 person running: medium-dark skin tone +1F3C3 1F3FF ; fully-qualified # 🏃🏿 E1.0 person running: dark skin tone +1F3C3 200D 2642 FE0F ; fully-qualified # 🏃♂️ E4.0 man running +1F3C3 200D 2642 ; minimally-qualified # 🏃♂ E4.0 man running +1F3C3 1F3FB 200D 2642 FE0F ; fully-qualified # 🏃🏻♂️ E4.0 man running: light skin tone +1F3C3 1F3FB 200D 2642 ; minimally-qualified # 🏃🏻♂ E4.0 man running: light skin tone +1F3C3 1F3FC 200D 2642 FE0F ; fully-qualified # 🏃🏼♂️ E4.0 man running: medium-light skin tone +1F3C3 1F3FC 200D 2642 ; minimally-qualified # 🏃🏼♂ E4.0 man running: medium-light skin tone +1F3C3 1F3FD 200D 2642 FE0F ; fully-qualified # 🏃🏽♂️ E4.0 man running: medium skin tone +1F3C3 1F3FD 200D 2642 ; minimally-qualified # 🏃🏽♂ E4.0 man running: medium skin tone +1F3C3 1F3FE 200D 2642 FE0F ; fully-qualified # 🏃🏾♂️ E4.0 man running: medium-dark skin tone +1F3C3 1F3FE 200D 2642 ; minimally-qualified # 🏃🏾♂ E4.0 man running: medium-dark skin tone +1F3C3 1F3FF 200D 2642 FE0F ; fully-qualified # 🏃🏿♂️ E4.0 man running: dark skin tone +1F3C3 1F3FF 200D 2642 ; minimally-qualified # 🏃🏿♂ E4.0 man running: dark skin tone +1F3C3 200D 2640 FE0F ; fully-qualified # 🏃♀️ E4.0 woman running +1F3C3 200D 2640 ; minimally-qualified # 🏃♀ E4.0 woman running +1F3C3 1F3FB 200D 2640 FE0F ; fully-qualified # 🏃🏻♀️ E4.0 woman running: light skin tone +1F3C3 1F3FB 200D 2640 ; minimally-qualified # 🏃🏻♀ E4.0 woman running: light skin tone +1F3C3 1F3FC 200D 2640 FE0F ; fully-qualified # 🏃🏼♀️ E4.0 woman running: medium-light skin tone +1F3C3 1F3FC 200D 2640 ; minimally-qualified # 🏃🏼♀ E4.0 woman running: medium-light skin tone +1F3C3 1F3FD 200D 2640 FE0F ; fully-qualified # 🏃🏽♀️ E4.0 woman running: medium skin tone +1F3C3 1F3FD 200D 2640 ; minimally-qualified # 🏃🏽♀ E4.0 woman running: medium skin tone +1F3C3 1F3FE 200D 2640 FE0F ; fully-qualified # 🏃🏾♀️ E4.0 woman running: medium-dark skin tone +1F3C3 1F3FE 200D 2640 ; minimally-qualified # 🏃🏾♀ E4.0 woman running: medium-dark skin tone +1F3C3 1F3FF 200D 2640 FE0F ; fully-qualified # 🏃🏿♀️ E4.0 woman running: dark skin tone +1F3C3 1F3FF 200D 2640 ; minimally-qualified # 🏃🏿♀ E4.0 woman running: dark skin tone +1F483 ; fully-qualified # 💃 E0.6 woman dancing +1F483 1F3FB ; fully-qualified # 💃🏻 E1.0 woman dancing: light skin tone +1F483 1F3FC ; fully-qualified # 💃🏼 E1.0 woman dancing: medium-light skin tone +1F483 1F3FD ; fully-qualified # 💃🏽 E1.0 woman dancing: medium skin tone +1F483 1F3FE ; fully-qualified # 💃🏾 E1.0 woman dancing: medium-dark skin tone +1F483 1F3FF ; fully-qualified # 💃🏿 E1.0 woman dancing: dark skin tone +1F57A ; fully-qualified # 🕺 E3.0 man dancing +1F57A 1F3FB ; fully-qualified # 🕺🏻 E3.0 man dancing: light skin tone +1F57A 1F3FC ; fully-qualified # 🕺🏼 E3.0 man dancing: medium-light skin tone +1F57A 1F3FD ; fully-qualified # 🕺🏽 E3.0 man dancing: medium skin tone +1F57A 1F3FE ; fully-qualified # 🕺🏾 E3.0 man dancing: medium-dark skin tone +1F57A 1F3FF ; fully-qualified # 🕺🏿 E3.0 man dancing: dark skin tone +1F574 FE0F ; fully-qualified # 🕴️ E0.7 man in suit levitating +1F574 ; unqualified # 🕴 E0.7 man in suit levitating +1F574 1F3FB ; fully-qualified # 🕴🏻 E4.0 man in suit levitating: light skin tone +1F574 1F3FC ; fully-qualified # 🕴🏼 E4.0 man in suit levitating: medium-light skin tone +1F574 1F3FD ; fully-qualified # 🕴🏽 E4.0 man in suit levitating: medium skin tone +1F574 1F3FE ; fully-qualified # 🕴🏾 E4.0 man in suit levitating: medium-dark skin tone +1F574 1F3FF ; fully-qualified # 🕴🏿 E4.0 man in suit levitating: dark skin tone +1F46F ; fully-qualified # 👯 E0.6 people with bunny ears +1F46F 200D 2642 FE0F ; fully-qualified # 👯♂️ E4.0 men with bunny ears +1F46F 200D 2642 ; minimally-qualified # 👯♂ E4.0 men with bunny ears +1F46F 200D 2640 FE0F ; fully-qualified # 👯♀️ E4.0 women with bunny ears +1F46F 200D 2640 ; minimally-qualified # 👯♀ E4.0 women with bunny ears +1F9D6 ; fully-qualified # 🧖 E5.0 person in steamy room +1F9D6 1F3FB ; fully-qualified # 🧖🏻 E5.0 person in steamy room: light skin tone +1F9D6 1F3FC ; fully-qualified # 🧖🏼 E5.0 person in steamy room: medium-light skin tone +1F9D6 1F3FD ; fully-qualified # 🧖🏽 E5.0 person in steamy room: medium skin tone +1F9D6 1F3FE ; fully-qualified # 🧖🏾 E5.0 person in steamy room: medium-dark skin tone +1F9D6 1F3FF ; fully-qualified # 🧖🏿 E5.0 person in steamy room: dark skin tone +1F9D6 200D 2642 FE0F ; fully-qualified # 🧖♂️ E5.0 man in steamy room +1F9D6 200D 2642 ; minimally-qualified # 🧖♂ E5.0 man in steamy room +1F9D6 1F3FB 200D 2642 FE0F ; fully-qualified # 🧖🏻♂️ E5.0 man in steamy room: light skin tone +1F9D6 1F3FB 200D 2642 ; minimally-qualified # 🧖🏻♂ E5.0 man in steamy room: light skin tone +1F9D6 1F3FC 200D 2642 FE0F ; fully-qualified # 🧖🏼♂️ E5.0 man in steamy room: medium-light skin tone +1F9D6 1F3FC 200D 2642 ; minimally-qualified # 🧖🏼♂ E5.0 man in steamy room: medium-light skin tone +1F9D6 1F3FD 200D 2642 FE0F ; fully-qualified # 🧖🏽♂️ E5.0 man in steamy room: medium skin tone +1F9D6 1F3FD 200D 2642 ; minimally-qualified # 🧖🏽♂ E5.0 man in steamy room: medium skin tone +1F9D6 1F3FE 200D 2642 FE0F ; fully-qualified # 🧖🏾♂️ E5.0 man in steamy room: medium-dark skin tone +1F9D6 1F3FE 200D 2642 ; minimally-qualified # 🧖🏾♂ E5.0 man in steamy room: medium-dark skin tone +1F9D6 1F3FF 200D 2642 FE0F ; fully-qualified # 🧖🏿♂️ E5.0 man in steamy room: dark skin tone +1F9D6 1F3FF 200D 2642 ; minimally-qualified # 🧖🏿♂ E5.0 man in steamy room: dark skin tone +1F9D6 200D 2640 FE0F ; fully-qualified # 🧖♀️ E5.0 woman in steamy room +1F9D6 200D 2640 ; minimally-qualified # 🧖♀ E5.0 woman in steamy room +1F9D6 1F3FB 200D 2640 FE0F ; fully-qualified # 🧖🏻♀️ E5.0 woman in steamy room: light skin tone +1F9D6 1F3FB 200D 2640 ; minimally-qualified # 🧖🏻♀ E5.0 woman in steamy room: light skin tone +1F9D6 1F3FC 200D 2640 FE0F ; fully-qualified # 🧖🏼♀️ E5.0 woman in steamy room: medium-light skin tone +1F9D6 1F3FC 200D 2640 ; minimally-qualified # 🧖🏼♀ E5.0 woman in steamy room: medium-light skin tone +1F9D6 1F3FD 200D 2640 FE0F ; fully-qualified # 🧖🏽♀️ E5.0 woman in steamy room: medium skin tone +1F9D6 1F3FD 200D 2640 ; minimally-qualified # 🧖🏽♀ E5.0 woman in steamy room: medium skin tone +1F9D6 1F3FE 200D 2640 FE0F ; fully-qualified # 🧖🏾♀️ E5.0 woman in steamy room: medium-dark skin tone +1F9D6 1F3FE 200D 2640 ; minimally-qualified # 🧖🏾♀ E5.0 woman in steamy room: medium-dark skin tone +1F9D6 1F3FF 200D 2640 FE0F ; fully-qualified # 🧖🏿♀️ E5.0 woman in steamy room: dark skin tone +1F9D6 1F3FF 200D 2640 ; minimally-qualified # 🧖🏿♀ E5.0 woman in steamy room: dark skin tone +1F9D7 ; fully-qualified # 🧗 E5.0 person climbing +1F9D7 1F3FB ; fully-qualified # 🧗🏻 E5.0 person climbing: light skin tone +1F9D7 1F3FC ; fully-qualified # 🧗🏼 E5.0 person climbing: medium-light skin tone +1F9D7 1F3FD ; fully-qualified # 🧗🏽 E5.0 person climbing: medium skin tone +1F9D7 1F3FE ; fully-qualified # 🧗🏾 E5.0 person climbing: medium-dark skin tone +1F9D7 1F3FF ; fully-qualified # 🧗🏿 E5.0 person climbing: dark skin tone +1F9D7 200D 2642 FE0F ; fully-qualified # 🧗♂️ E5.0 man climbing +1F9D7 200D 2642 ; minimally-qualified # 🧗♂ E5.0 man climbing +1F9D7 1F3FB 200D 2642 FE0F ; fully-qualified # 🧗🏻♂️ E5.0 man climbing: light skin tone +1F9D7 1F3FB 200D 2642 ; minimally-qualified # 🧗🏻♂ E5.0 man climbing: light skin tone +1F9D7 1F3FC 200D 2642 FE0F ; fully-qualified # 🧗🏼♂️ E5.0 man climbing: medium-light skin tone +1F9D7 1F3FC 200D 2642 ; minimally-qualified # 🧗🏼♂ E5.0 man climbing: medium-light skin tone +1F9D7 1F3FD 200D 2642 FE0F ; fully-qualified # 🧗🏽♂️ E5.0 man climbing: medium skin tone +1F9D7 1F3FD 200D 2642 ; minimally-qualified # 🧗🏽♂ E5.0 man climbing: medium skin tone +1F9D7 1F3FE 200D 2642 FE0F ; fully-qualified # 🧗🏾♂️ E5.0 man climbing: medium-dark skin tone +1F9D7 1F3FE 200D 2642 ; minimally-qualified # 🧗🏾♂ E5.0 man climbing: medium-dark skin tone +1F9D7 1F3FF 200D 2642 FE0F ; fully-qualified # 🧗🏿♂️ E5.0 man climbing: dark skin tone +1F9D7 1F3FF 200D 2642 ; minimally-qualified # 🧗🏿♂ E5.0 man climbing: dark skin tone +1F9D7 200D 2640 FE0F ; fully-qualified # 🧗♀️ E5.0 woman climbing +1F9D7 200D 2640 ; minimally-qualified # 🧗♀ E5.0 woman climbing +1F9D7 1F3FB 200D 2640 FE0F ; fully-qualified # 🧗🏻♀️ E5.0 woman climbing: light skin tone +1F9D7 1F3FB 200D 2640 ; minimally-qualified # 🧗🏻♀ E5.0 woman climbing: light skin tone +1F9D7 1F3FC 200D 2640 FE0F ; fully-qualified # 🧗🏼♀️ E5.0 woman climbing: medium-light skin tone +1F9D7 1F3FC 200D 2640 ; minimally-qualified # 🧗🏼♀ E5.0 woman climbing: medium-light skin tone +1F9D7 1F3FD 200D 2640 FE0F ; fully-qualified # 🧗🏽♀️ E5.0 woman climbing: medium skin tone +1F9D7 1F3FD 200D 2640 ; minimally-qualified # 🧗🏽♀ E5.0 woman climbing: medium skin tone +1F9D7 1F3FE 200D 2640 FE0F ; fully-qualified # 🧗🏾♀️ E5.0 woman climbing: medium-dark skin tone +1F9D7 1F3FE 200D 2640 ; minimally-qualified # 🧗🏾♀ E5.0 woman climbing: medium-dark skin tone +1F9D7 1F3FF 200D 2640 FE0F ; fully-qualified # 🧗🏿♀️ E5.0 woman climbing: dark skin tone +1F9D7 1F3FF 200D 2640 ; minimally-qualified # 🧗🏿♀ E5.0 woman climbing: dark skin tone +1F977 ; fully-qualified # 🥷 E13.0 ninja + +# subgroup: person-sport +1F93A ; fully-qualified # 🤺 E3.0 person fencing +1F3C7 ; fully-qualified # 🏇 E1.0 horse racing +1F3C7 1F3FB ; fully-qualified # 🏇🏻 E1.0 horse racing: light skin tone +1F3C7 1F3FC ; fully-qualified # 🏇🏼 E1.0 horse racing: medium-light skin tone +1F3C7 1F3FD ; fully-qualified # 🏇🏽 E1.0 horse racing: medium skin tone +1F3C7 1F3FE ; fully-qualified # 🏇🏾 E1.0 horse racing: medium-dark skin tone +1F3C7 1F3FF ; fully-qualified # 🏇🏿 E1.0 horse racing: dark skin tone +26F7 FE0F ; fully-qualified # ⛷️ E0.7 skier +26F7 ; unqualified # ⛷ E0.7 skier +1F3C2 ; fully-qualified # 🏂 E0.6 snowboarder +1F3C2 1F3FB ; fully-qualified # 🏂🏻 E1.0 snowboarder: light skin tone +1F3C2 1F3FC ; fully-qualified # 🏂🏼 E1.0 snowboarder: medium-light skin tone +1F3C2 1F3FD ; fully-qualified # 🏂🏽 E1.0 snowboarder: medium skin tone +1F3C2 1F3FE ; fully-qualified # 🏂🏾 E1.0 snowboarder: medium-dark skin tone +1F3C2 1F3FF ; fully-qualified # 🏂🏿 E1.0 snowboarder: dark skin tone +1F3CC FE0F ; fully-qualified # 🏌️ E0.7 person golfing +1F3CC ; unqualified # 🏌 E0.7 person golfing +1F3CC 1F3FB ; fully-qualified # 🏌🏻 E4.0 person golfing: light skin tone +1F3CC 1F3FC ; fully-qualified # 🏌🏼 E4.0 person golfing: medium-light skin tone +1F3CC 1F3FD ; fully-qualified # 🏌🏽 E4.0 person golfing: medium skin tone +1F3CC 1F3FE ; fully-qualified # 🏌🏾 E4.0 person golfing: medium-dark skin tone +1F3CC 1F3FF ; fully-qualified # 🏌🏿 E4.0 person golfing: dark skin tone +1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️♂️ E4.0 man golfing +1F3CC 200D 2642 FE0F ; unqualified # 🏌♂️ E4.0 man golfing +1F3CC FE0F 200D 2642 ; unqualified # 🏌️♂ E4.0 man golfing +1F3CC 200D 2642 ; unqualified # 🏌♂ E4.0 man golfing +1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻♂️ E4.0 man golfing: light skin tone +1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻♂ E4.0 man golfing: light skin tone +1F3CC 1F3FC 200D 2642 FE0F ; fully-qualified # 🏌🏼♂️ E4.0 man golfing: medium-light skin tone +1F3CC 1F3FC 200D 2642 ; minimally-qualified # 🏌🏼♂ E4.0 man golfing: medium-light skin tone +1F3CC 1F3FD 200D 2642 FE0F ; fully-qualified # 🏌🏽♂️ E4.0 man golfing: medium skin tone +1F3CC 1F3FD 200D 2642 ; minimally-qualified # 🏌🏽♂ E4.0 man golfing: medium skin tone +1F3CC 1F3FE 200D 2642 FE0F ; fully-qualified # 🏌🏾♂️ E4.0 man golfing: medium-dark skin tone +1F3CC 1F3FE 200D 2642 ; minimally-qualified # 🏌🏾♂ E4.0 man golfing: medium-dark skin tone +1F3CC 1F3FF 200D 2642 FE0F ; fully-qualified # 🏌🏿♂️ E4.0 man golfing: dark skin tone +1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿♂ E4.0 man golfing: dark skin tone +1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️♀️ E4.0 woman golfing +1F3CC 200D 2640 FE0F ; unqualified # 🏌♀️ E4.0 woman golfing +1F3CC FE0F 200D 2640 ; unqualified # 🏌️♀ E4.0 woman golfing +1F3CC 200D 2640 ; unqualified # 🏌♀ E4.0 woman golfing +1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻♀️ E4.0 woman golfing: light skin tone +1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻♀ E4.0 woman golfing: light skin tone +1F3CC 1F3FC 200D 2640 FE0F ; fully-qualified # 🏌🏼♀️ E4.0 woman golfing: medium-light skin tone +1F3CC 1F3FC 200D 2640 ; minimally-qualified # 🏌🏼♀ E4.0 woman golfing: medium-light skin tone +1F3CC 1F3FD 200D 2640 FE0F ; fully-qualified # 🏌🏽♀️ E4.0 woman golfing: medium skin tone +1F3CC 1F3FD 200D 2640 ; minimally-qualified # 🏌🏽♀ E4.0 woman golfing: medium skin tone +1F3CC 1F3FE 200D 2640 FE0F ; fully-qualified # 🏌🏾♀️ E4.0 woman golfing: medium-dark skin tone +1F3CC 1F3FE 200D 2640 ; minimally-qualified # 🏌🏾♀ E4.0 woman golfing: medium-dark skin tone +1F3CC 1F3FF 200D 2640 FE0F ; fully-qualified # 🏌🏿♀️ E4.0 woman golfing: dark skin tone +1F3CC 1F3FF 200D 2640 ; minimally-qualified # 🏌🏿♀ E4.0 woman golfing: dark skin tone +1F3C4 ; fully-qualified # 🏄 E0.6 person surfing +1F3C4 1F3FB ; fully-qualified # 🏄🏻 E1.0 person surfing: light skin tone +1F3C4 1F3FC ; fully-qualified # 🏄🏼 E1.0 person surfing: medium-light skin tone +1F3C4 1F3FD ; fully-qualified # 🏄🏽 E1.0 person surfing: medium skin tone +1F3C4 1F3FE ; fully-qualified # 🏄🏾 E1.0 person surfing: medium-dark skin tone +1F3C4 1F3FF ; fully-qualified # 🏄🏿 E1.0 person surfing: dark skin tone +1F3C4 200D 2642 FE0F ; fully-qualified # 🏄♂️ E4.0 man surfing +1F3C4 200D 2642 ; minimally-qualified # 🏄♂ E4.0 man surfing +1F3C4 1F3FB 200D 2642 FE0F ; fully-qualified # 🏄🏻♂️ E4.0 man surfing: light skin tone +1F3C4 1F3FB 200D 2642 ; minimally-qualified # 🏄🏻♂ E4.0 man surfing: light skin tone +1F3C4 1F3FC 200D 2642 FE0F ; fully-qualified # 🏄🏼♂️ E4.0 man surfing: medium-light skin tone +1F3C4 1F3FC 200D 2642 ; minimally-qualified # 🏄🏼♂ E4.0 man surfing: medium-light skin tone +1F3C4 1F3FD 200D 2642 FE0F ; fully-qualified # 🏄🏽♂️ E4.0 man surfing: medium skin tone +1F3C4 1F3FD 200D 2642 ; minimally-qualified # 🏄🏽♂ E4.0 man surfing: medium skin tone +1F3C4 1F3FE 200D 2642 FE0F ; fully-qualified # 🏄🏾♂️ E4.0 man surfing: medium-dark skin tone +1F3C4 1F3FE 200D 2642 ; minimally-qualified # 🏄🏾♂ E4.0 man surfing: medium-dark skin tone +1F3C4 1F3FF 200D 2642 FE0F ; fully-qualified # 🏄🏿♂️ E4.0 man surfing: dark skin tone +1F3C4 1F3FF 200D 2642 ; minimally-qualified # 🏄🏿♂ E4.0 man surfing: dark skin tone +1F3C4 200D 2640 FE0F ; fully-qualified # 🏄♀️ E4.0 woman surfing +1F3C4 200D 2640 ; minimally-qualified # 🏄♀ E4.0 woman surfing +1F3C4 1F3FB 200D 2640 FE0F ; fully-qualified # 🏄🏻♀️ E4.0 woman surfing: light skin tone +1F3C4 1F3FB 200D 2640 ; minimally-qualified # 🏄🏻♀ E4.0 woman surfing: light skin tone +1F3C4 1F3FC 200D 2640 FE0F ; fully-qualified # 🏄🏼♀️ E4.0 woman surfing: medium-light skin tone +1F3C4 1F3FC 200D 2640 ; minimally-qualified # 🏄🏼♀ E4.0 woman surfing: medium-light skin tone +1F3C4 1F3FD 200D 2640 FE0F ; fully-qualified # 🏄🏽♀️ E4.0 woman surfing: medium skin tone +1F3C4 1F3FD 200D 2640 ; minimally-qualified # 🏄🏽♀ E4.0 woman surfing: medium skin tone +1F3C4 1F3FE 200D 2640 FE0F ; fully-qualified # 🏄🏾♀️ E4.0 woman surfing: medium-dark skin tone +1F3C4 1F3FE 200D 2640 ; minimally-qualified # 🏄🏾♀ E4.0 woman surfing: medium-dark skin tone +1F3C4 1F3FF 200D 2640 FE0F ; fully-qualified # 🏄🏿♀️ E4.0 woman surfing: dark skin tone +1F3C4 1F3FF 200D 2640 ; minimally-qualified # 🏄🏿♀ E4.0 woman surfing: dark skin tone +1F6A3 ; fully-qualified # 🚣 E1.0 person rowing boat +1F6A3 1F3FB ; fully-qualified # 🚣🏻 E1.0 person rowing boat: light skin tone +1F6A3 1F3FC ; fully-qualified # 🚣🏼 E1.0 person rowing boat: medium-light skin tone +1F6A3 1F3FD ; fully-qualified # 🚣🏽 E1.0 person rowing boat: medium skin tone +1F6A3 1F3FE ; fully-qualified # 🚣🏾 E1.0 person rowing boat: medium-dark skin tone +1F6A3 1F3FF ; fully-qualified # 🚣🏿 E1.0 person rowing boat: dark skin tone +1F6A3 200D 2642 FE0F ; fully-qualified # 🚣♂️ E4.0 man rowing boat +1F6A3 200D 2642 ; minimally-qualified # 🚣♂ E4.0 man rowing boat +1F6A3 1F3FB 200D 2642 FE0F ; fully-qualified # 🚣🏻♂️ E4.0 man rowing boat: light skin tone +1F6A3 1F3FB 200D 2642 ; minimally-qualified # 🚣🏻♂ E4.0 man rowing boat: light skin tone +1F6A3 1F3FC 200D 2642 FE0F ; fully-qualified # 🚣🏼♂️ E4.0 man rowing boat: medium-light skin tone +1F6A3 1F3FC 200D 2642 ; minimally-qualified # 🚣🏼♂ E4.0 man rowing boat: medium-light skin tone +1F6A3 1F3FD 200D 2642 FE0F ; fully-qualified # 🚣🏽♂️ E4.0 man rowing boat: medium skin tone +1F6A3 1F3FD 200D 2642 ; minimally-qualified # 🚣🏽♂ E4.0 man rowing boat: medium skin tone +1F6A3 1F3FE 200D 2642 FE0F ; fully-qualified # 🚣🏾♂️ E4.0 man rowing boat: medium-dark skin tone +1F6A3 1F3FE 200D 2642 ; minimally-qualified # 🚣🏾♂ E4.0 man rowing boat: medium-dark skin tone +1F6A3 1F3FF 200D 2642 FE0F ; fully-qualified # 🚣🏿♂️ E4.0 man rowing boat: dark skin tone +1F6A3 1F3FF 200D 2642 ; minimally-qualified # 🚣🏿♂ E4.0 man rowing boat: dark skin tone +1F6A3 200D 2640 FE0F ; fully-qualified # 🚣♀️ E4.0 woman rowing boat +1F6A3 200D 2640 ; minimally-qualified # 🚣♀ E4.0 woman rowing boat +1F6A3 1F3FB 200D 2640 FE0F ; fully-qualified # 🚣🏻♀️ E4.0 woman rowing boat: light skin tone +1F6A3 1F3FB 200D 2640 ; minimally-qualified # 🚣🏻♀ E4.0 woman rowing boat: light skin tone +1F6A3 1F3FC 200D 2640 FE0F ; fully-qualified # 🚣🏼♀️ E4.0 woman rowing boat: medium-light skin tone +1F6A3 1F3FC 200D 2640 ; minimally-qualified # 🚣🏼♀ E4.0 woman rowing boat: medium-light skin tone +1F6A3 1F3FD 200D 2640 FE0F ; fully-qualified # 🚣🏽♀️ E4.0 woman rowing boat: medium skin tone +1F6A3 1F3FD 200D 2640 ; minimally-qualified # 🚣🏽♀ E4.0 woman rowing boat: medium skin tone +1F6A3 1F3FE 200D 2640 FE0F ; fully-qualified # 🚣🏾♀️ E4.0 woman rowing boat: medium-dark skin tone +1F6A3 1F3FE 200D 2640 ; minimally-qualified # 🚣🏾♀ E4.0 woman rowing boat: medium-dark skin tone +1F6A3 1F3FF 200D 2640 FE0F ; fully-qualified # 🚣🏿♀️ E4.0 woman rowing boat: dark skin tone +1F6A3 1F3FF 200D 2640 ; minimally-qualified # 🚣🏿♀ E4.0 woman rowing boat: dark skin tone +1F3CA ; fully-qualified # 🏊 E0.6 person swimming +1F3CA 1F3FB ; fully-qualified # 🏊🏻 E1.0 person swimming: light skin tone +1F3CA 1F3FC ; fully-qualified # 🏊🏼 E1.0 person swimming: medium-light skin tone +1F3CA 1F3FD ; fully-qualified # 🏊🏽 E1.0 person swimming: medium skin tone +1F3CA 1F3FE ; fully-qualified # 🏊🏾 E1.0 person swimming: medium-dark skin tone +1F3CA 1F3FF ; fully-qualified # 🏊🏿 E1.0 person swimming: dark skin tone +1F3CA 200D 2642 FE0F ; fully-qualified # 🏊♂️ E4.0 man swimming +1F3CA 200D 2642 ; minimally-qualified # 🏊♂ E4.0 man swimming +1F3CA 1F3FB 200D 2642 FE0F ; fully-qualified # 🏊🏻♂️ E4.0 man swimming: light skin tone +1F3CA 1F3FB 200D 2642 ; minimally-qualified # 🏊🏻♂ E4.0 man swimming: light skin tone +1F3CA 1F3FC 200D 2642 FE0F ; fully-qualified # 🏊🏼♂️ E4.0 man swimming: medium-light skin tone +1F3CA 1F3FC 200D 2642 ; minimally-qualified # 🏊🏼♂ E4.0 man swimming: medium-light skin tone +1F3CA 1F3FD 200D 2642 FE0F ; fully-qualified # 🏊🏽♂️ E4.0 man swimming: medium skin tone +1F3CA 1F3FD 200D 2642 ; minimally-qualified # 🏊🏽♂ E4.0 man swimming: medium skin tone +1F3CA 1F3FE 200D 2642 FE0F ; fully-qualified # 🏊🏾♂️ E4.0 man swimming: medium-dark skin tone +1F3CA 1F3FE 200D 2642 ; minimally-qualified # 🏊🏾♂ E4.0 man swimming: medium-dark skin tone +1F3CA 1F3FF 200D 2642 FE0F ; fully-qualified # 🏊🏿♂️ E4.0 man swimming: dark skin tone +1F3CA 1F3FF 200D 2642 ; minimally-qualified # 🏊🏿♂ E4.0 man swimming: dark skin tone +1F3CA 200D 2640 FE0F ; fully-qualified # 🏊♀️ E4.0 woman swimming +1F3CA 200D 2640 ; minimally-qualified # 🏊♀ E4.0 woman swimming +1F3CA 1F3FB 200D 2640 FE0F ; fully-qualified # 🏊🏻♀️ E4.0 woman swimming: light skin tone +1F3CA 1F3FB 200D 2640 ; minimally-qualified # 🏊🏻♀ E4.0 woman swimming: light skin tone +1F3CA 1F3FC 200D 2640 FE0F ; fully-qualified # 🏊🏼♀️ E4.0 woman swimming: medium-light skin tone +1F3CA 1F3FC 200D 2640 ; minimally-qualified # 🏊🏼♀ E4.0 woman swimming: medium-light skin tone +1F3CA 1F3FD 200D 2640 FE0F ; fully-qualified # 🏊🏽♀️ E4.0 woman swimming: medium skin tone +1F3CA 1F3FD 200D 2640 ; minimally-qualified # 🏊🏽♀ E4.0 woman swimming: medium skin tone +1F3CA 1F3FE 200D 2640 FE0F ; fully-qualified # 🏊🏾♀️ E4.0 woman swimming: medium-dark skin tone +1F3CA 1F3FE 200D 2640 ; minimally-qualified # 🏊🏾♀ E4.0 woman swimming: medium-dark skin tone +1F3CA 1F3FF 200D 2640 FE0F ; fully-qualified # 🏊🏿♀️ E4.0 woman swimming: dark skin tone +1F3CA 1F3FF 200D 2640 ; minimally-qualified # 🏊🏿♀ E4.0 woman swimming: dark skin tone +26F9 FE0F ; fully-qualified # ⛹️ E0.7 person bouncing ball +26F9 ; unqualified # ⛹ E0.7 person bouncing ball +26F9 1F3FB ; fully-qualified # ⛹🏻 E2.0 person bouncing ball: light skin tone +26F9 1F3FC ; fully-qualified # ⛹🏼 E2.0 person bouncing ball: medium-light skin tone +26F9 1F3FD ; fully-qualified # ⛹🏽 E2.0 person bouncing ball: medium skin tone +26F9 1F3FE ; fully-qualified # ⛹🏾 E2.0 person bouncing ball: medium-dark skin tone +26F9 1F3FF ; fully-qualified # ⛹🏿 E2.0 person bouncing ball: dark skin tone +26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️♂️ E4.0 man bouncing ball +26F9 200D 2642 FE0F ; unqualified # ⛹♂️ E4.0 man bouncing ball +26F9 FE0F 200D 2642 ; unqualified # ⛹️♂ E4.0 man bouncing ball +26F9 200D 2642 ; unqualified # ⛹♂ E4.0 man bouncing ball +26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻♂️ E4.0 man bouncing ball: light skin tone +26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻♂ E4.0 man bouncing ball: light skin tone +26F9 1F3FC 200D 2642 FE0F ; fully-qualified # ⛹🏼♂️ E4.0 man bouncing ball: medium-light skin tone +26F9 1F3FC 200D 2642 ; minimally-qualified # ⛹🏼♂ E4.0 man bouncing ball: medium-light skin tone +26F9 1F3FD 200D 2642 FE0F ; fully-qualified # ⛹🏽♂️ E4.0 man bouncing ball: medium skin tone +26F9 1F3FD 200D 2642 ; minimally-qualified # ⛹🏽♂ E4.0 man bouncing ball: medium skin tone +26F9 1F3FE 200D 2642 FE0F ; fully-qualified # ⛹🏾♂️ E4.0 man bouncing ball: medium-dark skin tone +26F9 1F3FE 200D 2642 ; minimally-qualified # ⛹🏾♂ E4.0 man bouncing ball: medium-dark skin tone +26F9 1F3FF 200D 2642 FE0F ; fully-qualified # ⛹🏿♂️ E4.0 man bouncing ball: dark skin tone +26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿♂ E4.0 man bouncing ball: dark skin tone +26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️♀️ E4.0 woman bouncing ball +26F9 200D 2640 FE0F ; unqualified # ⛹♀️ E4.0 woman bouncing ball +26F9 FE0F 200D 2640 ; unqualified # ⛹️♀ E4.0 woman bouncing ball +26F9 200D 2640 ; unqualified # ⛹♀ E4.0 woman bouncing ball +26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻♀️ E4.0 woman bouncing ball: light skin tone +26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻♀ E4.0 woman bouncing ball: light skin tone +26F9 1F3FC 200D 2640 FE0F ; fully-qualified # ⛹🏼♀️ E4.0 woman bouncing ball: medium-light skin tone +26F9 1F3FC 200D 2640 ; minimally-qualified # ⛹🏼♀ E4.0 woman bouncing ball: medium-light skin tone +26F9 1F3FD 200D 2640 FE0F ; fully-qualified # ⛹🏽♀️ E4.0 woman bouncing ball: medium skin tone +26F9 1F3FD 200D 2640 ; minimally-qualified # ⛹🏽♀ E4.0 woman bouncing ball: medium skin tone +26F9 1F3FE 200D 2640 FE0F ; fully-qualified # ⛹🏾♀️ E4.0 woman bouncing ball: medium-dark skin tone +26F9 1F3FE 200D 2640 ; minimally-qualified # ⛹🏾♀ E4.0 woman bouncing ball: medium-dark skin tone +26F9 1F3FF 200D 2640 FE0F ; fully-qualified # ⛹🏿♀️ E4.0 woman bouncing ball: dark skin tone +26F9 1F3FF 200D 2640 ; minimally-qualified # ⛹🏿♀ E4.0 woman bouncing ball: dark skin tone +1F3CB FE0F ; fully-qualified # 🏋️ E0.7 person lifting weights +1F3CB ; unqualified # 🏋 E0.7 person lifting weights +1F3CB 1F3FB ; fully-qualified # 🏋🏻 E2.0 person lifting weights: light skin tone +1F3CB 1F3FC ; fully-qualified # 🏋🏼 E2.0 person lifting weights: medium-light skin tone +1F3CB 1F3FD ; fully-qualified # 🏋🏽 E2.0 person lifting weights: medium skin tone +1F3CB 1F3FE ; fully-qualified # 🏋🏾 E2.0 person lifting weights: medium-dark skin tone +1F3CB 1F3FF ; fully-qualified # 🏋🏿 E2.0 person lifting weights: dark skin tone +1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️♂️ E4.0 man lifting weights +1F3CB 200D 2642 FE0F ; unqualified # 🏋♂️ E4.0 man lifting weights +1F3CB FE0F 200D 2642 ; unqualified # 🏋️♂ E4.0 man lifting weights +1F3CB 200D 2642 ; unqualified # 🏋♂ E4.0 man lifting weights +1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻♂️ E4.0 man lifting weights: light skin tone +1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻♂ E4.0 man lifting weights: light skin tone +1F3CB 1F3FC 200D 2642 FE0F ; fully-qualified # 🏋🏼♂️ E4.0 man lifting weights: medium-light skin tone +1F3CB 1F3FC 200D 2642 ; minimally-qualified # 🏋🏼♂ E4.0 man lifting weights: medium-light skin tone +1F3CB 1F3FD 200D 2642 FE0F ; fully-qualified # 🏋🏽♂️ E4.0 man lifting weights: medium skin tone +1F3CB 1F3FD 200D 2642 ; minimally-qualified # 🏋🏽♂ E4.0 man lifting weights: medium skin tone +1F3CB 1F3FE 200D 2642 FE0F ; fully-qualified # 🏋🏾♂️ E4.0 man lifting weights: medium-dark skin tone +1F3CB 1F3FE 200D 2642 ; minimally-qualified # 🏋🏾♂ E4.0 man lifting weights: medium-dark skin tone +1F3CB 1F3FF 200D 2642 FE0F ; fully-qualified # 🏋🏿♂️ E4.0 man lifting weights: dark skin tone +1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿♂ E4.0 man lifting weights: dark skin tone +1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️♀️ E4.0 woman lifting weights +1F3CB 200D 2640 FE0F ; unqualified # 🏋♀️ E4.0 woman lifting weights +1F3CB FE0F 200D 2640 ; unqualified # 🏋️♀ E4.0 woman lifting weights +1F3CB 200D 2640 ; unqualified # 🏋♀ E4.0 woman lifting weights +1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻♀️ E4.0 woman lifting weights: light skin tone +1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻♀ E4.0 woman lifting weights: light skin tone +1F3CB 1F3FC 200D 2640 FE0F ; fully-qualified # 🏋🏼♀️ E4.0 woman lifting weights: medium-light skin tone +1F3CB 1F3FC 200D 2640 ; minimally-qualified # 🏋🏼♀ E4.0 woman lifting weights: medium-light skin tone +1F3CB 1F3FD 200D 2640 FE0F ; fully-qualified # 🏋🏽♀️ E4.0 woman lifting weights: medium skin tone +1F3CB 1F3FD 200D 2640 ; minimally-qualified # 🏋🏽♀ E4.0 woman lifting weights: medium skin tone +1F3CB 1F3FE 200D 2640 FE0F ; fully-qualified # 🏋🏾♀️ E4.0 woman lifting weights: medium-dark skin tone +1F3CB 1F3FE 200D 2640 ; minimally-qualified # 🏋🏾♀ E4.0 woman lifting weights: medium-dark skin tone +1F3CB 1F3FF 200D 2640 FE0F ; fully-qualified # 🏋🏿♀️ E4.0 woman lifting weights: dark skin tone +1F3CB 1F3FF 200D 2640 ; minimally-qualified # 🏋🏿♀ E4.0 woman lifting weights: dark skin tone +1F6B4 ; fully-qualified # 🚴 E1.0 person biking +1F6B4 1F3FB ; fully-qualified # 🚴🏻 E1.0 person biking: light skin tone +1F6B4 1F3FC ; fully-qualified # 🚴🏼 E1.0 person biking: medium-light skin tone +1F6B4 1F3FD ; fully-qualified # 🚴🏽 E1.0 person biking: medium skin tone +1F6B4 1F3FE ; fully-qualified # 🚴🏾 E1.0 person biking: medium-dark skin tone +1F6B4 1F3FF ; fully-qualified # 🚴🏿 E1.0 person biking: dark skin tone +1F6B4 200D 2642 FE0F ; fully-qualified # 🚴♂️ E4.0 man biking +1F6B4 200D 2642 ; minimally-qualified # 🚴♂ E4.0 man biking +1F6B4 1F3FB 200D 2642 FE0F ; fully-qualified # 🚴🏻♂️ E4.0 man biking: light skin tone +1F6B4 1F3FB 200D 2642 ; minimally-qualified # 🚴🏻♂ E4.0 man biking: light skin tone +1F6B4 1F3FC 200D 2642 FE0F ; fully-qualified # 🚴🏼♂️ E4.0 man biking: medium-light skin tone +1F6B4 1F3FC 200D 2642 ; minimally-qualified # 🚴🏼♂ E4.0 man biking: medium-light skin tone +1F6B4 1F3FD 200D 2642 FE0F ; fully-qualified # 🚴🏽♂️ E4.0 man biking: medium skin tone +1F6B4 1F3FD 200D 2642 ; minimally-qualified # 🚴🏽♂ E4.0 man biking: medium skin tone +1F6B4 1F3FE 200D 2642 FE0F ; fully-qualified # 🚴🏾♂️ E4.0 man biking: medium-dark skin tone +1F6B4 1F3FE 200D 2642 ; minimally-qualified # 🚴🏾♂ E4.0 man biking: medium-dark skin tone +1F6B4 1F3FF 200D 2642 FE0F ; fully-qualified # 🚴🏿♂️ E4.0 man biking: dark skin tone +1F6B4 1F3FF 200D 2642 ; minimally-qualified # 🚴🏿♂ E4.0 man biking: dark skin tone +1F6B4 200D 2640 FE0F ; fully-qualified # 🚴♀️ E4.0 woman biking +1F6B4 200D 2640 ; minimally-qualified # 🚴♀ E4.0 woman biking +1F6B4 1F3FB 200D 2640 FE0F ; fully-qualified # 🚴🏻♀️ E4.0 woman biking: light skin tone +1F6B4 1F3FB 200D 2640 ; minimally-qualified # 🚴🏻♀ E4.0 woman biking: light skin tone +1F6B4 1F3FC 200D 2640 FE0F ; fully-qualified # 🚴🏼♀️ E4.0 woman biking: medium-light skin tone +1F6B4 1F3FC 200D 2640 ; minimally-qualified # 🚴🏼♀ E4.0 woman biking: medium-light skin tone +1F6B4 1F3FD 200D 2640 FE0F ; fully-qualified # 🚴🏽♀️ E4.0 woman biking: medium skin tone +1F6B4 1F3FD 200D 2640 ; minimally-qualified # 🚴🏽♀ E4.0 woman biking: medium skin tone +1F6B4 1F3FE 200D 2640 FE0F ; fully-qualified # 🚴🏾♀️ E4.0 woman biking: medium-dark skin tone +1F6B4 1F3FE 200D 2640 ; minimally-qualified # 🚴🏾♀ E4.0 woman biking: medium-dark skin tone +1F6B4 1F3FF 200D 2640 FE0F ; fully-qualified # 🚴🏿♀️ E4.0 woman biking: dark skin tone +1F6B4 1F3FF 200D 2640 ; minimally-qualified # 🚴🏿♀ E4.0 woman biking: dark skin tone +1F6B5 ; fully-qualified # 🚵 E1.0 person mountain biking +1F6B5 1F3FB ; fully-qualified # 🚵🏻 E1.0 person mountain biking: light skin tone +1F6B5 1F3FC ; fully-qualified # 🚵🏼 E1.0 person mountain biking: medium-light skin tone +1F6B5 1F3FD ; fully-qualified # 🚵🏽 E1.0 person mountain biking: medium skin tone +1F6B5 1F3FE ; fully-qualified # 🚵🏾 E1.0 person mountain biking: medium-dark skin tone +1F6B5 1F3FF ; fully-qualified # 🚵🏿 E1.0 person mountain biking: dark skin tone +1F6B5 200D 2642 FE0F ; fully-qualified # 🚵♂️ E4.0 man mountain biking +1F6B5 200D 2642 ; minimally-qualified # 🚵♂ E4.0 man mountain biking +1F6B5 1F3FB 200D 2642 FE0F ; fully-qualified # 🚵🏻♂️ E4.0 man mountain biking: light skin tone +1F6B5 1F3FB 200D 2642 ; minimally-qualified # 🚵🏻♂ E4.0 man mountain biking: light skin tone +1F6B5 1F3FC 200D 2642 FE0F ; fully-qualified # 🚵🏼♂️ E4.0 man mountain biking: medium-light skin tone +1F6B5 1F3FC 200D 2642 ; minimally-qualified # 🚵🏼♂ E4.0 man mountain biking: medium-light skin tone +1F6B5 1F3FD 200D 2642 FE0F ; fully-qualified # 🚵🏽♂️ E4.0 man mountain biking: medium skin tone +1F6B5 1F3FD 200D 2642 ; minimally-qualified # 🚵🏽♂ E4.0 man mountain biking: medium skin tone +1F6B5 1F3FE 200D 2642 FE0F ; fully-qualified # 🚵🏾♂️ E4.0 man mountain biking: medium-dark skin tone +1F6B5 1F3FE 200D 2642 ; minimally-qualified # 🚵🏾♂ E4.0 man mountain biking: medium-dark skin tone +1F6B5 1F3FF 200D 2642 FE0F ; fully-qualified # 🚵🏿♂️ E4.0 man mountain biking: dark skin tone +1F6B5 1F3FF 200D 2642 ; minimally-qualified # 🚵🏿♂ E4.0 man mountain biking: dark skin tone +1F6B5 200D 2640 FE0F ; fully-qualified # 🚵♀️ E4.0 woman mountain biking +1F6B5 200D 2640 ; minimally-qualified # 🚵♀ E4.0 woman mountain biking +1F6B5 1F3FB 200D 2640 FE0F ; fully-qualified # 🚵🏻♀️ E4.0 woman mountain biking: light skin tone +1F6B5 1F3FB 200D 2640 ; minimally-qualified # 🚵🏻♀ E4.0 woman mountain biking: light skin tone +1F6B5 1F3FC 200D 2640 FE0F ; fully-qualified # 🚵🏼♀️ E4.0 woman mountain biking: medium-light skin tone +1F6B5 1F3FC 200D 2640 ; minimally-qualified # 🚵🏼♀ E4.0 woman mountain biking: medium-light skin tone +1F6B5 1F3FD 200D 2640 FE0F ; fully-qualified # 🚵🏽♀️ E4.0 woman mountain biking: medium skin tone +1F6B5 1F3FD 200D 2640 ; minimally-qualified # 🚵🏽♀ E4.0 woman mountain biking: medium skin tone +1F6B5 1F3FE 200D 2640 FE0F ; fully-qualified # 🚵🏾♀️ E4.0 woman mountain biking: medium-dark skin tone +1F6B5 1F3FE 200D 2640 ; minimally-qualified # 🚵🏾♀ E4.0 woman mountain biking: medium-dark skin tone +1F6B5 1F3FF 200D 2640 FE0F ; fully-qualified # 🚵🏿♀️ E4.0 woman mountain biking: dark skin tone +1F6B5 1F3FF 200D 2640 ; minimally-qualified # 🚵🏿♀ E4.0 woman mountain biking: dark skin tone +1F938 ; fully-qualified # 🤸 E3.0 person cartwheeling +1F938 1F3FB ; fully-qualified # 🤸🏻 E3.0 person cartwheeling: light skin tone +1F938 1F3FC ; fully-qualified # 🤸🏼 E3.0 person cartwheeling: medium-light skin tone +1F938 1F3FD ; fully-qualified # 🤸🏽 E3.0 person cartwheeling: medium skin tone +1F938 1F3FE ; fully-qualified # 🤸🏾 E3.0 person cartwheeling: medium-dark skin tone +1F938 1F3FF ; fully-qualified # 🤸🏿 E3.0 person cartwheeling: dark skin tone +1F938 200D 2642 FE0F ; fully-qualified # 🤸♂️ E4.0 man cartwheeling +1F938 200D 2642 ; minimally-qualified # 🤸♂ E4.0 man cartwheeling +1F938 1F3FB 200D 2642 FE0F ; fully-qualified # 🤸🏻♂️ E4.0 man cartwheeling: light skin tone +1F938 1F3FB 200D 2642 ; minimally-qualified # 🤸🏻♂ E4.0 man cartwheeling: light skin tone +1F938 1F3FC 200D 2642 FE0F ; fully-qualified # 🤸🏼♂️ E4.0 man cartwheeling: medium-light skin tone +1F938 1F3FC 200D 2642 ; minimally-qualified # 🤸🏼♂ E4.0 man cartwheeling: medium-light skin tone +1F938 1F3FD 200D 2642 FE0F ; fully-qualified # 🤸🏽♂️ E4.0 man cartwheeling: medium skin tone +1F938 1F3FD 200D 2642 ; minimally-qualified # 🤸🏽♂ E4.0 man cartwheeling: medium skin tone +1F938 1F3FE 200D 2642 FE0F ; fully-qualified # 🤸🏾♂️ E4.0 man cartwheeling: medium-dark skin tone +1F938 1F3FE 200D 2642 ; minimally-qualified # 🤸🏾♂ E4.0 man cartwheeling: medium-dark skin tone +1F938 1F3FF 200D 2642 FE0F ; fully-qualified # 🤸🏿♂️ E4.0 man cartwheeling: dark skin tone +1F938 1F3FF 200D 2642 ; minimally-qualified # 🤸🏿♂ E4.0 man cartwheeling: dark skin tone +1F938 200D 2640 FE0F ; fully-qualified # 🤸♀️ E4.0 woman cartwheeling +1F938 200D 2640 ; minimally-qualified # 🤸♀ E4.0 woman cartwheeling +1F938 1F3FB 200D 2640 FE0F ; fully-qualified # 🤸🏻♀️ E4.0 woman cartwheeling: light skin tone +1F938 1F3FB 200D 2640 ; minimally-qualified # 🤸🏻♀ E4.0 woman cartwheeling: light skin tone +1F938 1F3FC 200D 2640 FE0F ; fully-qualified # 🤸🏼♀️ E4.0 woman cartwheeling: medium-light skin tone +1F938 1F3FC 200D 2640 ; minimally-qualified # 🤸🏼♀ E4.0 woman cartwheeling: medium-light skin tone +1F938 1F3FD 200D 2640 FE0F ; fully-qualified # 🤸🏽♀️ E4.0 woman cartwheeling: medium skin tone +1F938 1F3FD 200D 2640 ; minimally-qualified # 🤸🏽♀ E4.0 woman cartwheeling: medium skin tone +1F938 1F3FE 200D 2640 FE0F ; fully-qualified # 🤸🏾♀️ E4.0 woman cartwheeling: medium-dark skin tone +1F938 1F3FE 200D 2640 ; minimally-qualified # 🤸🏾♀ E4.0 woman cartwheeling: medium-dark skin tone +1F938 1F3FF 200D 2640 FE0F ; fully-qualified # 🤸🏿♀️ E4.0 woman cartwheeling: dark skin tone +1F938 1F3FF 200D 2640 ; minimally-qualified # 🤸🏿♀ E4.0 woman cartwheeling: dark skin tone +1F93C ; fully-qualified # 🤼 E3.0 people wrestling +1F93C 200D 2642 FE0F ; fully-qualified # 🤼♂️ E4.0 men wrestling +1F93C 200D 2642 ; minimally-qualified # 🤼♂ E4.0 men wrestling +1F93C 200D 2640 FE0F ; fully-qualified # 🤼♀️ E4.0 women wrestling +1F93C 200D 2640 ; minimally-qualified # 🤼♀ E4.0 women wrestling +1F93D ; fully-qualified # 🤽 E3.0 person playing water polo +1F93D 1F3FB ; fully-qualified # 🤽🏻 E3.0 person playing water polo: light skin tone +1F93D 1F3FC ; fully-qualified # 🤽🏼 E3.0 person playing water polo: medium-light skin tone +1F93D 1F3FD ; fully-qualified # 🤽🏽 E3.0 person playing water polo: medium skin tone +1F93D 1F3FE ; fully-qualified # 🤽🏾 E3.0 person playing water polo: medium-dark skin tone +1F93D 1F3FF ; fully-qualified # 🤽🏿 E3.0 person playing water polo: dark skin tone +1F93D 200D 2642 FE0F ; fully-qualified # 🤽♂️ E4.0 man playing water polo +1F93D 200D 2642 ; minimally-qualified # 🤽♂ E4.0 man playing water polo +1F93D 1F3FB 200D 2642 FE0F ; fully-qualified # 🤽🏻♂️ E4.0 man playing water polo: light skin tone +1F93D 1F3FB 200D 2642 ; minimally-qualified # 🤽🏻♂ E4.0 man playing water polo: light skin tone +1F93D 1F3FC 200D 2642 FE0F ; fully-qualified # 🤽🏼♂️ E4.0 man playing water polo: medium-light skin tone +1F93D 1F3FC 200D 2642 ; minimally-qualified # 🤽🏼♂ E4.0 man playing water polo: medium-light skin tone +1F93D 1F3FD 200D 2642 FE0F ; fully-qualified # 🤽🏽♂️ E4.0 man playing water polo: medium skin tone +1F93D 1F3FD 200D 2642 ; minimally-qualified # 🤽🏽♂ E4.0 man playing water polo: medium skin tone +1F93D 1F3FE 200D 2642 FE0F ; fully-qualified # 🤽🏾♂️ E4.0 man playing water polo: medium-dark skin tone +1F93D 1F3FE 200D 2642 ; minimally-qualified # 🤽🏾♂ E4.0 man playing water polo: medium-dark skin tone +1F93D 1F3FF 200D 2642 FE0F ; fully-qualified # 🤽🏿♂️ E4.0 man playing water polo: dark skin tone +1F93D 1F3FF 200D 2642 ; minimally-qualified # 🤽🏿♂ E4.0 man playing water polo: dark skin tone +1F93D 200D 2640 FE0F ; fully-qualified # 🤽♀️ E4.0 woman playing water polo +1F93D 200D 2640 ; minimally-qualified # 🤽♀ E4.0 woman playing water polo +1F93D 1F3FB 200D 2640 FE0F ; fully-qualified # 🤽🏻♀️ E4.0 woman playing water polo: light skin tone +1F93D 1F3FB 200D 2640 ; minimally-qualified # 🤽🏻♀ E4.0 woman playing water polo: light skin tone +1F93D 1F3FC 200D 2640 FE0F ; fully-qualified # 🤽🏼♀️ E4.0 woman playing water polo: medium-light skin tone +1F93D 1F3FC 200D 2640 ; minimally-qualified # 🤽🏼♀ E4.0 woman playing water polo: medium-light skin tone +1F93D 1F3FD 200D 2640 FE0F ; fully-qualified # 🤽🏽♀️ E4.0 woman playing water polo: medium skin tone +1F93D 1F3FD 200D 2640 ; minimally-qualified # 🤽🏽♀ E4.0 woman playing water polo: medium skin tone +1F93D 1F3FE 200D 2640 FE0F ; fully-qualified # 🤽🏾♀️ E4.0 woman playing water polo: medium-dark skin tone +1F93D 1F3FE 200D 2640 ; minimally-qualified # 🤽🏾♀ E4.0 woman playing water polo: medium-dark skin tone +1F93D 1F3FF 200D 2640 FE0F ; fully-qualified # 🤽🏿♀️ E4.0 woman playing water polo: dark skin tone +1F93D 1F3FF 200D 2640 ; minimally-qualified # 🤽🏿♀ E4.0 woman playing water polo: dark skin tone +1F93E ; fully-qualified # 🤾 E3.0 person playing handball +1F93E 1F3FB ; fully-qualified # 🤾🏻 E3.0 person playing handball: light skin tone +1F93E 1F3FC ; fully-qualified # 🤾🏼 E3.0 person playing handball: medium-light skin tone +1F93E 1F3FD ; fully-qualified # 🤾🏽 E3.0 person playing handball: medium skin tone +1F93E 1F3FE ; fully-qualified # 🤾🏾 E3.0 person playing handball: medium-dark skin tone +1F93E 1F3FF ; fully-qualified # 🤾🏿 E3.0 person playing handball: dark skin tone +1F93E 200D 2642 FE0F ; fully-qualified # 🤾♂️ E4.0 man playing handball +1F93E 200D 2642 ; minimally-qualified # 🤾♂ E4.0 man playing handball +1F93E 1F3FB 200D 2642 FE0F ; fully-qualified # 🤾🏻♂️ E4.0 man playing handball: light skin tone +1F93E 1F3FB 200D 2642 ; minimally-qualified # 🤾🏻♂ E4.0 man playing handball: light skin tone +1F93E 1F3FC 200D 2642 FE0F ; fully-qualified # 🤾🏼♂️ E4.0 man playing handball: medium-light skin tone +1F93E 1F3FC 200D 2642 ; minimally-qualified # 🤾🏼♂ E4.0 man playing handball: medium-light skin tone +1F93E 1F3FD 200D 2642 FE0F ; fully-qualified # 🤾🏽♂️ E4.0 man playing handball: medium skin tone +1F93E 1F3FD 200D 2642 ; minimally-qualified # 🤾🏽♂ E4.0 man playing handball: medium skin tone +1F93E 1F3FE 200D 2642 FE0F ; fully-qualified # 🤾🏾♂️ E4.0 man playing handball: medium-dark skin tone +1F93E 1F3FE 200D 2642 ; minimally-qualified # 🤾🏾♂ E4.0 man playing handball: medium-dark skin tone +1F93E 1F3FF 200D 2642 FE0F ; fully-qualified # 🤾🏿♂️ E4.0 man playing handball: dark skin tone +1F93E 1F3FF 200D 2642 ; minimally-qualified # 🤾🏿♂ E4.0 man playing handball: dark skin tone +1F93E 200D 2640 FE0F ; fully-qualified # 🤾♀️ E4.0 woman playing handball +1F93E 200D 2640 ; minimally-qualified # 🤾♀ E4.0 woman playing handball +1F93E 1F3FB 200D 2640 FE0F ; fully-qualified # 🤾🏻♀️ E4.0 woman playing handball: light skin tone +1F93E 1F3FB 200D 2640 ; minimally-qualified # 🤾🏻♀ E4.0 woman playing handball: light skin tone +1F93E 1F3FC 200D 2640 FE0F ; fully-qualified # 🤾🏼♀️ E4.0 woman playing handball: medium-light skin tone +1F93E 1F3FC 200D 2640 ; minimally-qualified # 🤾🏼♀ E4.0 woman playing handball: medium-light skin tone +1F93E 1F3FD 200D 2640 FE0F ; fully-qualified # 🤾🏽♀️ E4.0 woman playing handball: medium skin tone +1F93E 1F3FD 200D 2640 ; minimally-qualified # 🤾🏽♀ E4.0 woman playing handball: medium skin tone +1F93E 1F3FE 200D 2640 FE0F ; fully-qualified # 🤾🏾♀️ E4.0 woman playing handball: medium-dark skin tone +1F93E 1F3FE 200D 2640 ; minimally-qualified # 🤾🏾♀ E4.0 woman playing handball: medium-dark skin tone +1F93E 1F3FF 200D 2640 FE0F ; fully-qualified # 🤾🏿♀️ E4.0 woman playing handball: dark skin tone +1F93E 1F3FF 200D 2640 ; minimally-qualified # 🤾🏿♀ E4.0 woman playing handball: dark skin tone +1F939 ; fully-qualified # 🤹 E3.0 person juggling +1F939 1F3FB ; fully-qualified # 🤹🏻 E3.0 person juggling: light skin tone +1F939 1F3FC ; fully-qualified # 🤹🏼 E3.0 person juggling: medium-light skin tone +1F939 1F3FD ; fully-qualified # 🤹🏽 E3.0 person juggling: medium skin tone +1F939 1F3FE ; fully-qualified # 🤹🏾 E3.0 person juggling: medium-dark skin tone +1F939 1F3FF ; fully-qualified # 🤹🏿 E3.0 person juggling: dark skin tone +1F939 200D 2642 FE0F ; fully-qualified # 🤹♂️ E4.0 man juggling +1F939 200D 2642 ; minimally-qualified # 🤹♂ E4.0 man juggling +1F939 1F3FB 200D 2642 FE0F ; fully-qualified # 🤹🏻♂️ E4.0 man juggling: light skin tone +1F939 1F3FB 200D 2642 ; minimally-qualified # 🤹🏻♂ E4.0 man juggling: light skin tone +1F939 1F3FC 200D 2642 FE0F ; fully-qualified # 🤹🏼♂️ E4.0 man juggling: medium-light skin tone +1F939 1F3FC 200D 2642 ; minimally-qualified # 🤹🏼♂ E4.0 man juggling: medium-light skin tone +1F939 1F3FD 200D 2642 FE0F ; fully-qualified # 🤹🏽♂️ E4.0 man juggling: medium skin tone +1F939 1F3FD 200D 2642 ; minimally-qualified # 🤹🏽♂ E4.0 man juggling: medium skin tone +1F939 1F3FE 200D 2642 FE0F ; fully-qualified # 🤹🏾♂️ E4.0 man juggling: medium-dark skin tone +1F939 1F3FE 200D 2642 ; minimally-qualified # 🤹🏾♂ E4.0 man juggling: medium-dark skin tone +1F939 1F3FF 200D 2642 FE0F ; fully-qualified # 🤹🏿♂️ E4.0 man juggling: dark skin tone +1F939 1F3FF 200D 2642 ; minimally-qualified # 🤹🏿♂ E4.0 man juggling: dark skin tone +1F939 200D 2640 FE0F ; fully-qualified # 🤹♀️ E4.0 woman juggling +1F939 200D 2640 ; minimally-qualified # 🤹♀ E4.0 woman juggling +1F939 1F3FB 200D 2640 FE0F ; fully-qualified # 🤹🏻♀️ E4.0 woman juggling: light skin tone +1F939 1F3FB 200D 2640 ; minimally-qualified # 🤹🏻♀ E4.0 woman juggling: light skin tone +1F939 1F3FC 200D 2640 FE0F ; fully-qualified # 🤹🏼♀️ E4.0 woman juggling: medium-light skin tone +1F939 1F3FC 200D 2640 ; minimally-qualified # 🤹🏼♀ E4.0 woman juggling: medium-light skin tone +1F939 1F3FD 200D 2640 FE0F ; fully-qualified # 🤹🏽♀️ E4.0 woman juggling: medium skin tone +1F939 1F3FD 200D 2640 ; minimally-qualified # 🤹🏽♀ E4.0 woman juggling: medium skin tone +1F939 1F3FE 200D 2640 FE0F ; fully-qualified # 🤹🏾♀️ E4.0 woman juggling: medium-dark skin tone +1F939 1F3FE 200D 2640 ; minimally-qualified # 🤹🏾♀ E4.0 woman juggling: medium-dark skin tone +1F939 1F3FF 200D 2640 FE0F ; fully-qualified # 🤹🏿♀️ E4.0 woman juggling: dark skin tone +1F939 1F3FF 200D 2640 ; minimally-qualified # 🤹🏿♀ E4.0 woman juggling: dark skin tone + +# subgroup: person-resting +1F9D8 ; fully-qualified # 🧘 E5.0 person in lotus position +1F9D8 1F3FB ; fully-qualified # 🧘🏻 E5.0 person in lotus position: light skin tone +1F9D8 1F3FC ; fully-qualified # 🧘🏼 E5.0 person in lotus position: medium-light skin tone +1F9D8 1F3FD ; fully-qualified # 🧘🏽 E5.0 person in lotus position: medium skin tone +1F9D8 1F3FE ; fully-qualified # 🧘🏾 E5.0 person in lotus position: medium-dark skin tone +1F9D8 1F3FF ; fully-qualified # 🧘🏿 E5.0 person in lotus position: dark skin tone +1F9D8 200D 2642 FE0F ; fully-qualified # 🧘♂️ E5.0 man in lotus position +1F9D8 200D 2642 ; minimally-qualified # 🧘♂ E5.0 man in lotus position +1F9D8 1F3FB 200D 2642 FE0F ; fully-qualified # 🧘🏻♂️ E5.0 man in lotus position: light skin tone +1F9D8 1F3FB 200D 2642 ; minimally-qualified # 🧘🏻♂ E5.0 man in lotus position: light skin tone +1F9D8 1F3FC 200D 2642 FE0F ; fully-qualified # 🧘🏼♂️ E5.0 man in lotus position: medium-light skin tone +1F9D8 1F3FC 200D 2642 ; minimally-qualified # 🧘🏼♂ E5.0 man in lotus position: medium-light skin tone +1F9D8 1F3FD 200D 2642 FE0F ; fully-qualified # 🧘🏽♂️ E5.0 man in lotus position: medium skin tone +1F9D8 1F3FD 200D 2642 ; minimally-qualified # 🧘🏽♂ E5.0 man in lotus position: medium skin tone +1F9D8 1F3FE 200D 2642 FE0F ; fully-qualified # 🧘🏾♂️ E5.0 man in lotus position: medium-dark skin tone +1F9D8 1F3FE 200D 2642 ; minimally-qualified # 🧘🏾♂ E5.0 man in lotus position: medium-dark skin tone +1F9D8 1F3FF 200D 2642 FE0F ; fully-qualified # 🧘🏿♂️ E5.0 man in lotus position: dark skin tone +1F9D8 1F3FF 200D 2642 ; minimally-qualified # 🧘🏿♂ E5.0 man in lotus position: dark skin tone +1F9D8 200D 2640 FE0F ; fully-qualified # 🧘♀️ E5.0 woman in lotus position +1F9D8 200D 2640 ; minimally-qualified # 🧘♀ E5.0 woman in lotus position +1F9D8 1F3FB 200D 2640 FE0F ; fully-qualified # 🧘🏻♀️ E5.0 woman in lotus position: light skin tone +1F9D8 1F3FB 200D 2640 ; minimally-qualified # 🧘🏻♀ E5.0 woman in lotus position: light skin tone +1F9D8 1F3FC 200D 2640 FE0F ; fully-qualified # 🧘🏼♀️ E5.0 woman in lotus position: medium-light skin tone +1F9D8 1F3FC 200D 2640 ; minimally-qualified # 🧘🏼♀ E5.0 woman in lotus position: medium-light skin tone +1F9D8 1F3FD 200D 2640 FE0F ; fully-qualified # 🧘🏽♀️ E5.0 woman in lotus position: medium skin tone +1F9D8 1F3FD 200D 2640 ; minimally-qualified # 🧘🏽♀ E5.0 woman in lotus position: medium skin tone +1F9D8 1F3FE 200D 2640 FE0F ; fully-qualified # 🧘🏾♀️ E5.0 woman in lotus position: medium-dark skin tone +1F9D8 1F3FE 200D 2640 ; minimally-qualified # 🧘🏾♀ E5.0 woman in lotus position: medium-dark skin tone +1F9D8 1F3FF 200D 2640 FE0F ; fully-qualified # 🧘🏿♀️ E5.0 woman in lotus position: dark skin tone +1F9D8 1F3FF 200D 2640 ; minimally-qualified # 🧘🏿♀ E5.0 woman in lotus position: dark skin tone +1F6C0 ; fully-qualified # 🛀 E0.6 person taking bath +1F6C0 1F3FB ; fully-qualified # 🛀🏻 E1.0 person taking bath: light skin tone +1F6C0 1F3FC ; fully-qualified # 🛀🏼 E1.0 person taking bath: medium-light skin tone +1F6C0 1F3FD ; fully-qualified # 🛀🏽 E1.0 person taking bath: medium skin tone +1F6C0 1F3FE ; fully-qualified # 🛀🏾 E1.0 person taking bath: medium-dark skin tone +1F6C0 1F3FF ; fully-qualified # 🛀🏿 E1.0 person taking bath: dark skin tone +1F6CC ; fully-qualified # 🛌 E1.0 person in bed +1F6CC 1F3FB ; fully-qualified # 🛌🏻 E4.0 person in bed: light skin tone +1F6CC 1F3FC ; fully-qualified # 🛌🏼 E4.0 person in bed: medium-light skin tone +1F6CC 1F3FD ; fully-qualified # 🛌🏽 E4.0 person in bed: medium skin tone +1F6CC 1F3FE ; fully-qualified # 🛌🏾 E4.0 person in bed: medium-dark skin tone +1F6CC 1F3FF ; fully-qualified # 🛌🏿 E4.0 person in bed: dark skin tone + +# subgroup: family +1F9D1 200D 1F91D 200D 1F9D1 ; fully-qualified # 🧑🤝🧑 E12.0 people holding hands +1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏻🤝🧑🏻 E12.0 people holding hands: light skin tone +1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻🤝🧑🏼 E12.1 people holding hands: light skin tone, medium-light skin tone +1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻🤝🧑🏽 E12.1 people holding hands: light skin tone, medium skin tone +1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻🤝🧑🏾 E12.1 people holding hands: light skin tone, medium-dark skin tone +1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻🤝🧑🏿 E12.1 people holding hands: light skin tone, dark skin tone +1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼🤝🧑🏻 E12.0 people holding hands: medium-light skin tone, light skin tone +1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏼🤝🧑🏼 E12.0 people holding hands: medium-light skin tone +1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼🤝🧑🏽 E12.1 people holding hands: medium-light skin tone, medium skin tone +1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼🤝🧑🏾 E12.1 people holding hands: medium-light skin tone, medium-dark skin tone +1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼🤝🧑🏿 E12.1 people holding hands: medium-light skin tone, dark skin tone +1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽🤝🧑🏻 E12.0 people holding hands: medium skin tone, light skin tone +1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽🤝🧑🏼 E12.0 people holding hands: medium skin tone, medium-light skin tone +1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏽🤝🧑🏽 E12.0 people holding hands: medium skin tone +1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽🤝🧑🏾 E12.1 people holding hands: medium skin tone, medium-dark skin tone +1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽🤝🧑🏿 E12.1 people holding hands: medium skin tone, dark skin tone +1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾🤝🧑🏻 E12.0 people holding hands: medium-dark skin tone, light skin tone +1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾🤝🧑🏼 E12.0 people holding hands: medium-dark skin tone, medium-light skin tone +1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾🤝🧑🏽 E12.0 people holding hands: medium-dark skin tone, medium skin tone +1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏾🤝🧑🏾 E12.0 people holding hands: medium-dark skin tone +1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾🤝🧑🏿 E12.1 people holding hands: medium-dark skin tone, dark skin tone +1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿🤝🧑🏻 E12.0 people holding hands: dark skin tone, light skin tone +1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿🤝🧑🏼 E12.0 people holding hands: dark skin tone, medium-light skin tone +1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿🤝🧑🏽 E12.0 people holding hands: dark skin tone, medium skin tone +1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿🤝🧑🏾 E12.0 people holding hands: dark skin tone, medium-dark skin tone +1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏿🤝🧑🏿 E12.0 people holding hands: dark skin tone +1F46D ; fully-qualified # 👭 E1.0 women holding hands +1F46D 1F3FB ; fully-qualified # 👭🏻 E12.0 women holding hands: light skin tone +1F469 1F3FB 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏻🤝👩🏼 E12.1 women holding hands: light skin tone, medium-light skin tone +1F469 1F3FB 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏻🤝👩🏽 E12.1 women holding hands: light skin tone, medium skin tone +1F469 1F3FB 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏻🤝👩🏾 E12.1 women holding hands: light skin tone, medium-dark skin tone +1F469 1F3FB 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏻🤝👩🏿 E12.1 women holding hands: light skin tone, dark skin tone +1F469 1F3FC 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏼🤝👩🏻 E12.0 women holding hands: medium-light skin tone, light skin tone +1F46D 1F3FC ; fully-qualified # 👭🏼 E12.0 women holding hands: medium-light skin tone +1F469 1F3FC 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏼🤝👩🏽 E12.1 women holding hands: medium-light skin tone, medium skin tone +1F469 1F3FC 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏼🤝👩🏾 E12.1 women holding hands: medium-light skin tone, medium-dark skin tone +1F469 1F3FC 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏼🤝👩🏿 E12.1 women holding hands: medium-light skin tone, dark skin tone +1F469 1F3FD 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏽🤝👩🏻 E12.0 women holding hands: medium skin tone, light skin tone +1F469 1F3FD 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏽🤝👩🏼 E12.0 women holding hands: medium skin tone, medium-light skin tone +1F46D 1F3FD ; fully-qualified # 👭🏽 E12.0 women holding hands: medium skin tone +1F469 1F3FD 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏽🤝👩🏾 E12.1 women holding hands: medium skin tone, medium-dark skin tone +1F469 1F3FD 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏽🤝👩🏿 E12.1 women holding hands: medium skin tone, dark skin tone +1F469 1F3FE 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏾🤝👩🏻 E12.0 women holding hands: medium-dark skin tone, light skin tone +1F469 1F3FE 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏾🤝👩🏼 E12.0 women holding hands: medium-dark skin tone, medium-light skin tone +1F469 1F3FE 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏾🤝👩🏽 E12.0 women holding hands: medium-dark skin tone, medium skin tone +1F46D 1F3FE ; fully-qualified # 👭🏾 E12.0 women holding hands: medium-dark skin tone +1F469 1F3FE 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏾🤝👩🏿 E12.1 women holding hands: medium-dark skin tone, dark skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏿🤝👩🏻 E12.0 women holding hands: dark skin tone, light skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏿🤝👩🏼 E12.0 women holding hands: dark skin tone, medium-light skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏿🤝👩🏽 E12.0 women holding hands: dark skin tone, medium skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏿🤝👩🏾 E12.0 women holding hands: dark skin tone, medium-dark skin tone +1F46D 1F3FF ; fully-qualified # 👭🏿 E12.0 women holding hands: dark skin tone +1F46B ; fully-qualified # 👫 E0.6 woman and man holding hands +1F46B 1F3FB ; fully-qualified # 👫🏻 E12.0 woman and man holding hands: light skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏻🤝👨🏼 E12.0 woman and man holding hands: light skin tone, medium-light skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏻🤝👨🏽 E12.0 woman and man holding hands: light skin tone, medium skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏻🤝👨🏾 E12.0 woman and man holding hands: light skin tone, medium-dark skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏻🤝👨🏿 E12.0 woman and man holding hands: light skin tone, dark skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏼🤝👨🏻 E12.0 woman and man holding hands: medium-light skin tone, light skin tone +1F46B 1F3FC ; fully-qualified # 👫🏼 E12.0 woman and man holding hands: medium-light skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏼🤝👨🏽 E12.0 woman and man holding hands: medium-light skin tone, medium skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏼🤝👨🏾 E12.0 woman and man holding hands: medium-light skin tone, medium-dark skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏼🤝👨🏿 E12.0 woman and man holding hands: medium-light skin tone, dark skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏽🤝👨🏻 E12.0 woman and man holding hands: medium skin tone, light skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏽🤝👨🏼 E12.0 woman and man holding hands: medium skin tone, medium-light skin tone +1F46B 1F3FD ; fully-qualified # 👫🏽 E12.0 woman and man holding hands: medium skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏽🤝👨🏾 E12.0 woman and man holding hands: medium skin tone, medium-dark skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏽🤝👨🏿 E12.0 woman and man holding hands: medium skin tone, dark skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏾🤝👨🏻 E12.0 woman and man holding hands: medium-dark skin tone, light skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏾🤝👨🏼 E12.0 woman and man holding hands: medium-dark skin tone, medium-light skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏾🤝👨🏽 E12.0 woman and man holding hands: medium-dark skin tone, medium skin tone +1F46B 1F3FE ; fully-qualified # 👫🏾 E12.0 woman and man holding hands: medium-dark skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏾🤝👨🏿 E12.0 woman and man holding hands: medium-dark skin tone, dark skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏿🤝👨🏻 E12.0 woman and man holding hands: dark skin tone, light skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏿🤝👨🏼 E12.0 woman and man holding hands: dark skin tone, medium-light skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏿🤝👨🏽 E12.0 woman and man holding hands: dark skin tone, medium skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏿🤝👨🏾 E12.0 woman and man holding hands: dark skin tone, medium-dark skin tone +1F46B 1F3FF ; fully-qualified # 👫🏿 E12.0 woman and man holding hands: dark skin tone +1F46C ; fully-qualified # 👬 E1.0 men holding hands +1F46C 1F3FB ; fully-qualified # 👬🏻 E12.0 men holding hands: light skin tone +1F468 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏻🤝👨🏼 E12.1 men holding hands: light skin tone, medium-light skin tone +1F468 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏻🤝👨🏽 E12.1 men holding hands: light skin tone, medium skin tone +1F468 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏻🤝👨🏾 E12.1 men holding hands: light skin tone, medium-dark skin tone +1F468 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏻🤝👨🏿 E12.1 men holding hands: light skin tone, dark skin tone +1F468 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏼🤝👨🏻 E12.0 men holding hands: medium-light skin tone, light skin tone +1F46C 1F3FC ; fully-qualified # 👬🏼 E12.0 men holding hands: medium-light skin tone +1F468 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏼🤝👨🏽 E12.1 men holding hands: medium-light skin tone, medium skin tone +1F468 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏼🤝👨🏾 E12.1 men holding hands: medium-light skin tone, medium-dark skin tone +1F468 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏼🤝👨🏿 E12.1 men holding hands: medium-light skin tone, dark skin tone +1F468 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏽🤝👨🏻 E12.0 men holding hands: medium skin tone, light skin tone +1F468 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏽🤝👨🏼 E12.0 men holding hands: medium skin tone, medium-light skin tone +1F46C 1F3FD ; fully-qualified # 👬🏽 E12.0 men holding hands: medium skin tone +1F468 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏽🤝👨🏾 E12.1 men holding hands: medium skin tone, medium-dark skin tone +1F468 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏽🤝👨🏿 E12.1 men holding hands: medium skin tone, dark skin tone +1F468 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏾🤝👨🏻 E12.0 men holding hands: medium-dark skin tone, light skin tone +1F468 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏾🤝👨🏼 E12.0 men holding hands: medium-dark skin tone, medium-light skin tone +1F468 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏾🤝👨🏽 E12.0 men holding hands: medium-dark skin tone, medium skin tone +1F46C 1F3FE ; fully-qualified # 👬🏾 E12.0 men holding hands: medium-dark skin tone +1F468 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏾🤝👨🏿 E12.1 men holding hands: medium-dark skin tone, dark skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏿🤝👨🏻 E12.0 men holding hands: dark skin tone, light skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏿🤝👨🏼 E12.0 men holding hands: dark skin tone, medium-light skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏿🤝👨🏽 E12.0 men holding hands: dark skin tone, medium skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏿🤝👨🏾 E12.0 men holding hands: dark skin tone, medium-dark skin tone +1F46C 1F3FF ; fully-qualified # 👬🏿 E12.0 men holding hands: dark skin tone +1F48F ; fully-qualified # 💏 E0.6 kiss +1F469 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👩❤️💋👨 E2.0 kiss: woman, man +1F469 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👩❤💋👨 E2.0 kiss: woman, man +1F468 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👨❤️💋👨 E2.0 kiss: man, man +1F468 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👨❤💋👨 E2.0 kiss: man, man +1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 ; fully-qualified # 👩❤️💋👩 E2.0 kiss: woman, woman +1F469 200D 2764 200D 1F48B 200D 1F469 ; minimally-qualified # 👩❤💋👩 E2.0 kiss: woman, woman +1F491 ; fully-qualified # 💑 E0.6 couple with heart +1F469 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👩❤️👨 E2.0 couple with heart: woman, man +1F469 200D 2764 200D 1F468 ; minimally-qualified # 👩❤👨 E2.0 couple with heart: woman, man +1F468 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👨❤️👨 E2.0 couple with heart: man, man +1F468 200D 2764 200D 1F468 ; minimally-qualified # 👨❤👨 E2.0 couple with heart: man, man +1F469 200D 2764 FE0F 200D 1F469 ; fully-qualified # 👩❤️👩 E2.0 couple with heart: woman, woman +1F469 200D 2764 200D 1F469 ; minimally-qualified # 👩❤👩 E2.0 couple with heart: woman, woman +1F46A ; fully-qualified # 👪 E0.6 family +1F468 200D 1F469 200D 1F466 ; fully-qualified # 👨👩👦 E2.0 family: man, woman, boy +1F468 200D 1F469 200D 1F467 ; fully-qualified # 👨👩👧 E2.0 family: man, woman, girl +1F468 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👨👩👧👦 E2.0 family: man, woman, girl, boy +1F468 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👨👩👦👦 E2.0 family: man, woman, boy, boy +1F468 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👨👩👧👧 E2.0 family: man, woman, girl, girl +1F468 200D 1F468 200D 1F466 ; fully-qualified # 👨👨👦 E2.0 family: man, man, boy +1F468 200D 1F468 200D 1F467 ; fully-qualified # 👨👨👧 E2.0 family: man, man, girl +1F468 200D 1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨👨👧👦 E2.0 family: man, man, girl, boy +1F468 200D 1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨👨👦👦 E2.0 family: man, man, boy, boy +1F468 200D 1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨👨👧👧 E2.0 family: man, man, girl, girl +1F469 200D 1F469 200D 1F466 ; fully-qualified # 👩👩👦 E2.0 family: woman, woman, boy +1F469 200D 1F469 200D 1F467 ; fully-qualified # 👩👩👧 E2.0 family: woman, woman, girl +1F469 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩👩👧👦 E2.0 family: woman, woman, girl, boy +1F469 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩👩👦👦 E2.0 family: woman, woman, boy, boy +1F469 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩👩👧👧 E2.0 family: woman, woman, girl, girl +1F468 200D 1F466 ; fully-qualified # 👨👦 E4.0 family: man, boy +1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨👦👦 E4.0 family: man, boy, boy +1F468 200D 1F467 ; fully-qualified # 👨👧 E4.0 family: man, girl +1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨👧👦 E4.0 family: man, girl, boy +1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨👧👧 E4.0 family: man, girl, girl +1F469 200D 1F466 ; fully-qualified # 👩👦 E4.0 family: woman, boy +1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩👦👦 E4.0 family: woman, boy, boy +1F469 200D 1F467 ; fully-qualified # 👩👧 E4.0 family: woman, girl +1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩👧👦 E4.0 family: woman, girl, boy +1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩👧👧 E4.0 family: woman, girl, girl + +# subgroup: person-symbol +1F5E3 FE0F ; fully-qualified # 🗣️ E0.7 speaking head +1F5E3 ; unqualified # 🗣 E0.7 speaking head +1F464 ; fully-qualified # 👤 E0.6 bust in silhouette +1F465 ; fully-qualified # 👥 E1.0 busts in silhouette +1FAC2 ; fully-qualified # 🫂 E13.0 people hugging +1F463 ; fully-qualified # 👣 E0.6 footprints + +# People & Body subtotal: 2480 +# People & Body subtotal: 490 w/o modifiers + +# group: Component + +# subgroup: skin-tone +1F3FB ; component # 🏻 E1.0 light skin tone +1F3FC ; component # 🏼 E1.0 medium-light skin tone +1F3FD ; component # 🏽 E1.0 medium skin tone +1F3FE ; component # 🏾 E1.0 medium-dark skin tone +1F3FF ; component # 🏿 E1.0 dark skin tone + +# subgroup: hair-style +1F9B0 ; component # 🦰 E11.0 red hair +1F9B1 ; component # 🦱 E11.0 curly hair +1F9B3 ; component # 🦳 E11.0 white hair +1F9B2 ; component # 🦲 E11.0 bald + +# Component subtotal: 9 +# Component subtotal: 4 w/o modifiers + +# group: Animals & Nature + +# subgroup: animal-mammal +1F435 ; fully-qualified # 🐵 E0.6 monkey face +1F412 ; fully-qualified # 🐒 E0.6 monkey +1F98D ; fully-qualified # 🦍 E3.0 gorilla +1F9A7 ; fully-qualified # 🦧 E12.0 orangutan +1F436 ; fully-qualified # 🐶 E0.6 dog face +1F415 ; fully-qualified # 🐕 E0.7 dog +1F9AE ; fully-qualified # 🦮 E12.0 guide dog +1F415 200D 1F9BA ; fully-qualified # 🐕🦺 E12.0 service dog +1F429 ; fully-qualified # 🐩 E0.6 poodle +1F43A ; fully-qualified # 🐺 E0.6 wolf +1F98A ; fully-qualified # 🦊 E3.0 fox +1F99D ; fully-qualified # 🦝 E11.0 raccoon +1F431 ; fully-qualified # 🐱 E0.6 cat face +1F408 ; fully-qualified # 🐈 E0.7 cat +1F408 200D 2B1B ; fully-qualified # 🐈⬛ E13.0 black cat +1F981 ; fully-qualified # 🦁 E1.0 lion +1F42F ; fully-qualified # 🐯 E0.6 tiger face +1F405 ; fully-qualified # 🐅 E1.0 tiger +1F406 ; fully-qualified # 🐆 E1.0 leopard +1F434 ; fully-qualified # 🐴 E0.6 horse face +1F40E ; fully-qualified # 🐎 E0.6 horse +1F984 ; fully-qualified # 🦄 E1.0 unicorn +1F993 ; fully-qualified # 🦓 E5.0 zebra +1F98C ; fully-qualified # 🦌 E3.0 deer +1F9AC ; fully-qualified # 🦬 E13.0 bison +1F42E ; fully-qualified # 🐮 E0.6 cow face +1F402 ; fully-qualified # 🐂 E1.0 ox +1F403 ; fully-qualified # 🐃 E1.0 water buffalo +1F404 ; fully-qualified # 🐄 E1.0 cow +1F437 ; fully-qualified # 🐷 E0.6 pig face +1F416 ; fully-qualified # 🐖 E1.0 pig +1F417 ; fully-qualified # 🐗 E0.6 boar +1F43D ; fully-qualified # 🐽 E0.6 pig nose +1F40F ; fully-qualified # 🐏 E1.0 ram +1F411 ; fully-qualified # 🐑 E0.6 ewe +1F410 ; fully-qualified # 🐐 E1.0 goat +1F42A ; fully-qualified # 🐪 E1.0 camel +1F42B ; fully-qualified # 🐫 E0.6 two-hump camel +1F999 ; fully-qualified # 🦙 E11.0 llama +1F992 ; fully-qualified # 🦒 E5.0 giraffe +1F418 ; fully-qualified # 🐘 E0.6 elephant +1F9A3 ; fully-qualified # 🦣 E13.0 mammoth +1F98F ; fully-qualified # 🦏 E3.0 rhinoceros +1F99B ; fully-qualified # 🦛 E11.0 hippopotamus +1F42D ; fully-qualified # 🐭 E0.6 mouse face +1F401 ; fully-qualified # 🐁 E1.0 mouse +1F400 ; fully-qualified # 🐀 E1.0 rat +1F439 ; fully-qualified # 🐹 E0.6 hamster +1F430 ; fully-qualified # 🐰 E0.6 rabbit face +1F407 ; fully-qualified # 🐇 E1.0 rabbit +1F43F FE0F ; fully-qualified # 🐿️ E0.7 chipmunk +1F43F ; unqualified # 🐿 E0.7 chipmunk +1F9AB ; fully-qualified # 🦫 E13.0 beaver +1F994 ; fully-qualified # 🦔 E5.0 hedgehog +1F987 ; fully-qualified # 🦇 E3.0 bat +1F43B ; fully-qualified # 🐻 E0.6 bear +1F43B 200D 2744 FE0F ; fully-qualified # 🐻❄️ E13.0 polar bear +1F43B 200D 2744 ; minimally-qualified # 🐻❄ E13.0 polar bear +1F428 ; fully-qualified # 🐨 E0.6 koala +1F43C ; fully-qualified # 🐼 E0.6 panda +1F9A5 ; fully-qualified # 🦥 E12.0 sloth +1F9A6 ; fully-qualified # 🦦 E12.0 otter +1F9A8 ; fully-qualified # 🦨 E12.0 skunk +1F998 ; fully-qualified # 🦘 E11.0 kangaroo +1F9A1 ; fully-qualified # 🦡 E11.0 badger +1F43E ; fully-qualified # 🐾 E0.6 paw prints + +# subgroup: animal-bird +1F983 ; fully-qualified # 🦃 E1.0 turkey +1F414 ; fully-qualified # 🐔 E0.6 chicken +1F413 ; fully-qualified # 🐓 E1.0 rooster +1F423 ; fully-qualified # 🐣 E0.6 hatching chick +1F424 ; fully-qualified # 🐤 E0.6 baby chick +1F425 ; fully-qualified # 🐥 E0.6 front-facing baby chick +1F426 ; fully-qualified # 🐦 E0.6 bird +1F427 ; fully-qualified # 🐧 E0.6 penguin +1F54A FE0F ; fully-qualified # 🕊️ E0.7 dove +1F54A ; unqualified # 🕊 E0.7 dove +1F985 ; fully-qualified # 🦅 E3.0 eagle +1F986 ; fully-qualified # 🦆 E3.0 duck +1F9A2 ; fully-qualified # 🦢 E11.0 swan +1F989 ; fully-qualified # 🦉 E3.0 owl +1F9A4 ; fully-qualified # 🦤 E13.0 dodo +1FAB6 ; fully-qualified # 🪶 E13.0 feather +1F9A9 ; fully-qualified # 🦩 E12.0 flamingo +1F99A ; fully-qualified # 🦚 E11.0 peacock +1F99C ; fully-qualified # 🦜 E11.0 parrot + +# subgroup: animal-amphibian +1F438 ; fully-qualified # 🐸 E0.6 frog + +# subgroup: animal-reptile +1F40A ; fully-qualified # 🐊 E1.0 crocodile +1F422 ; fully-qualified # 🐢 E0.6 turtle +1F98E ; fully-qualified # 🦎 E3.0 lizard +1F40D ; fully-qualified # 🐍 E0.6 snake +1F432 ; fully-qualified # 🐲 E0.6 dragon face +1F409 ; fully-qualified # 🐉 E1.0 dragon +1F995 ; fully-qualified # 🦕 E5.0 sauropod +1F996 ; fully-qualified # 🦖 E5.0 T-Rex + +# subgroup: animal-marine +1F433 ; fully-qualified # 🐳 E0.6 spouting whale +1F40B ; fully-qualified # 🐋 E1.0 whale +1F42C ; fully-qualified # 🐬 E0.6 dolphin +1F9AD ; fully-qualified # 🦭 E13.0 seal +1F41F ; fully-qualified # 🐟 E0.6 fish +1F420 ; fully-qualified # 🐠 E0.6 tropical fish +1F421 ; fully-qualified # 🐡 E0.6 blowfish +1F988 ; fully-qualified # 🦈 E3.0 shark +1F419 ; fully-qualified # 🐙 E0.6 octopus +1F41A ; fully-qualified # 🐚 E0.6 spiral shell + +# subgroup: animal-bug +1F40C ; fully-qualified # 🐌 E0.6 snail +1F98B ; fully-qualified # 🦋 E3.0 butterfly +1F41B ; fully-qualified # 🐛 E0.6 bug +1F41C ; fully-qualified # 🐜 E0.6 ant +1F41D ; fully-qualified # 🐝 E0.6 honeybee +1FAB2 ; fully-qualified # 🪲 E13.0 beetle +1F41E ; fully-qualified # 🐞 E0.6 lady beetle +1F997 ; fully-qualified # 🦗 E5.0 cricket +1FAB3 ; fully-qualified # 🪳 E13.0 cockroach +1F577 FE0F ; fully-qualified # 🕷️ E0.7 spider +1F577 ; unqualified # 🕷 E0.7 spider +1F578 FE0F ; fully-qualified # 🕸️ E0.7 spider web +1F578 ; unqualified # 🕸 E0.7 spider web +1F982 ; fully-qualified # 🦂 E1.0 scorpion +1F99F ; fully-qualified # 🦟 E11.0 mosquito +1FAB0 ; fully-qualified # 🪰 E13.0 fly +1FAB1 ; fully-qualified # 🪱 E13.0 worm +1F9A0 ; fully-qualified # 🦠 E11.0 microbe + +# subgroup: plant-flower +1F490 ; fully-qualified # 💐 E0.6 bouquet +1F338 ; fully-qualified # 🌸 E0.6 cherry blossom +1F4AE ; fully-qualified # 💮 E0.6 white flower +1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette +1F3F5 ; unqualified # 🏵 E0.7 rosette +1F339 ; fully-qualified # 🌹 E0.6 rose +1F940 ; fully-qualified # 🥀 E3.0 wilted flower +1F33A ; fully-qualified # 🌺 E0.6 hibiscus +1F33B ; fully-qualified # 🌻 E0.6 sunflower +1F33C ; fully-qualified # 🌼 E0.6 blossom +1F337 ; fully-qualified # 🌷 E0.6 tulip + +# subgroup: plant-other +1F331 ; fully-qualified # 🌱 E0.6 seedling +1FAB4 ; fully-qualified # 🪴 E13.0 potted plant +1F332 ; fully-qualified # 🌲 E1.0 evergreen tree +1F333 ; fully-qualified # 🌳 E1.0 deciduous tree +1F334 ; fully-qualified # 🌴 E0.6 palm tree +1F335 ; fully-qualified # 🌵 E0.6 cactus +1F33E ; fully-qualified # 🌾 E0.6 sheaf of rice +1F33F ; fully-qualified # 🌿 E0.6 herb +2618 FE0F ; fully-qualified # ☘️ E1.0 shamrock +2618 ; unqualified # ☘ E1.0 shamrock +1F340 ; fully-qualified # 🍀 E0.6 four leaf clover +1F341 ; fully-qualified # 🍁 E0.6 maple leaf +1F342 ; fully-qualified # 🍂 E0.6 fallen leaf +1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind + +# Animals & Nature subtotal: 147 +# Animals & Nature subtotal: 147 w/o modifiers + +# group: Food & Drink + +# subgroup: food-fruit +1F347 ; fully-qualified # 🍇 E0.6 grapes +1F348 ; fully-qualified # 🍈 E0.6 melon +1F349 ; fully-qualified # 🍉 E0.6 watermelon +1F34A ; fully-qualified # 🍊 E0.6 tangerine +1F34B ; fully-qualified # 🍋 E1.0 lemon +1F34C ; fully-qualified # 🍌 E0.6 banana +1F34D ; fully-qualified # 🍍 E0.6 pineapple +1F96D ; fully-qualified # 🥭 E11.0 mango +1F34E ; fully-qualified # 🍎 E0.6 red apple +1F34F ; fully-qualified # 🍏 E0.6 green apple +1F350 ; fully-qualified # 🍐 E1.0 pear +1F351 ; fully-qualified # 🍑 E0.6 peach +1F352 ; fully-qualified # 🍒 E0.6 cherries +1F353 ; fully-qualified # 🍓 E0.6 strawberry +1FAD0 ; fully-qualified # 🫐 E13.0 blueberries +1F95D ; fully-qualified # 🥝 E3.0 kiwi fruit +1F345 ; fully-qualified # 🍅 E0.6 tomato +1FAD2 ; fully-qualified # 🫒 E13.0 olive +1F965 ; fully-qualified # 🥥 E5.0 coconut + +# subgroup: food-vegetable +1F951 ; fully-qualified # 🥑 E3.0 avocado +1F346 ; fully-qualified # 🍆 E0.6 eggplant +1F954 ; fully-qualified # 🥔 E3.0 potato +1F955 ; fully-qualified # 🥕 E3.0 carrot +1F33D ; fully-qualified # 🌽 E0.6 ear of corn +1F336 FE0F ; fully-qualified # 🌶️ E0.7 hot pepper +1F336 ; unqualified # 🌶 E0.7 hot pepper +1FAD1 ; fully-qualified # 🫑 E13.0 bell pepper +1F952 ; fully-qualified # 🥒 E3.0 cucumber +1F96C ; fully-qualified # 🥬 E11.0 leafy green +1F966 ; fully-qualified # 🥦 E5.0 broccoli +1F9C4 ; fully-qualified # 🧄 E12.0 garlic +1F9C5 ; fully-qualified # 🧅 E12.0 onion +1F344 ; fully-qualified # 🍄 E0.6 mushroom +1F95C ; fully-qualified # 🥜 E3.0 peanuts +1F330 ; fully-qualified # 🌰 E0.6 chestnut + +# subgroup: food-prepared +1F35E ; fully-qualified # 🍞 E0.6 bread +1F950 ; fully-qualified # 🥐 E3.0 croissant +1F956 ; fully-qualified # 🥖 E3.0 baguette bread +1FAD3 ; fully-qualified # 🫓 E13.0 flatbread +1F968 ; fully-qualified # 🥨 E5.0 pretzel +1F96F ; fully-qualified # 🥯 E11.0 bagel +1F95E ; fully-qualified # 🥞 E3.0 pancakes +1F9C7 ; fully-qualified # 🧇 E12.0 waffle +1F9C0 ; fully-qualified # 🧀 E1.0 cheese wedge +1F356 ; fully-qualified # 🍖 E0.6 meat on bone +1F357 ; fully-qualified # 🍗 E0.6 poultry leg +1F969 ; fully-qualified # 🥩 E5.0 cut of meat +1F953 ; fully-qualified # 🥓 E3.0 bacon +1F354 ; fully-qualified # 🍔 E0.6 hamburger +1F35F ; fully-qualified # 🍟 E0.6 french fries +1F355 ; fully-qualified # 🍕 E0.6 pizza +1F32D ; fully-qualified # 🌭 E1.0 hot dog +1F96A ; fully-qualified # 🥪 E5.0 sandwich +1F32E ; fully-qualified # 🌮 E1.0 taco +1F32F ; fully-qualified # 🌯 E1.0 burrito +1FAD4 ; fully-qualified # 🫔 E13.0 tamale +1F959 ; fully-qualified # 🥙 E3.0 stuffed flatbread +1F9C6 ; fully-qualified # 🧆 E12.0 falafel +1F95A ; fully-qualified # 🥚 E3.0 egg +1F373 ; fully-qualified # 🍳 E0.6 cooking +1F958 ; fully-qualified # 🥘 E3.0 shallow pan of food +1F372 ; fully-qualified # 🍲 E0.6 pot of food +1FAD5 ; fully-qualified # 🫕 E13.0 fondue +1F963 ; fully-qualified # 🥣 E5.0 bowl with spoon +1F957 ; fully-qualified # 🥗 E3.0 green salad +1F37F ; fully-qualified # 🍿 E1.0 popcorn +1F9C8 ; fully-qualified # 🧈 E12.0 butter +1F9C2 ; fully-qualified # 🧂 E11.0 salt +1F96B ; fully-qualified # 🥫 E5.0 canned food + +# subgroup: food-asian +1F371 ; fully-qualified # 🍱 E0.6 bento box +1F358 ; fully-qualified # 🍘 E0.6 rice cracker +1F359 ; fully-qualified # 🍙 E0.6 rice ball +1F35A ; fully-qualified # 🍚 E0.6 cooked rice +1F35B ; fully-qualified # 🍛 E0.6 curry rice +1F35C ; fully-qualified # 🍜 E0.6 steaming bowl +1F35D ; fully-qualified # 🍝 E0.6 spaghetti +1F360 ; fully-qualified # 🍠 E0.6 roasted sweet potato +1F362 ; fully-qualified # 🍢 E0.6 oden +1F363 ; fully-qualified # 🍣 E0.6 sushi +1F364 ; fully-qualified # 🍤 E0.6 fried shrimp +1F365 ; fully-qualified # 🍥 E0.6 fish cake with swirl +1F96E ; fully-qualified # 🥮 E11.0 moon cake +1F361 ; fully-qualified # 🍡 E0.6 dango +1F95F ; fully-qualified # 🥟 E5.0 dumpling +1F960 ; fully-qualified # 🥠 E5.0 fortune cookie +1F961 ; fully-qualified # 🥡 E5.0 takeout box + +# subgroup: food-marine +1F980 ; fully-qualified # 🦀 E1.0 crab +1F99E ; fully-qualified # 🦞 E11.0 lobster +1F990 ; fully-qualified # 🦐 E3.0 shrimp +1F991 ; fully-qualified # 🦑 E3.0 squid +1F9AA ; fully-qualified # 🦪 E12.0 oyster + +# subgroup: food-sweet +1F366 ; fully-qualified # 🍦 E0.6 soft ice cream +1F367 ; fully-qualified # 🍧 E0.6 shaved ice +1F368 ; fully-qualified # 🍨 E0.6 ice cream +1F369 ; fully-qualified # 🍩 E0.6 doughnut +1F36A ; fully-qualified # 🍪 E0.6 cookie +1F382 ; fully-qualified # 🎂 E0.6 birthday cake +1F370 ; fully-qualified # 🍰 E0.6 shortcake +1F9C1 ; fully-qualified # 🧁 E11.0 cupcake +1F967 ; fully-qualified # 🥧 E5.0 pie +1F36B ; fully-qualified # 🍫 E0.6 chocolate bar +1F36C ; fully-qualified # 🍬 E0.6 candy +1F36D ; fully-qualified # 🍭 E0.6 lollipop +1F36E ; fully-qualified # 🍮 E0.6 custard +1F36F ; fully-qualified # 🍯 E0.6 honey pot + +# subgroup: drink +1F37C ; fully-qualified # 🍼 E1.0 baby bottle +1F95B ; fully-qualified # 🥛 E3.0 glass of milk +2615 ; fully-qualified # ☕ E0.6 hot beverage +1FAD6 ; fully-qualified # 🫖 E13.0 teapot +1F375 ; fully-qualified # 🍵 E0.6 teacup without handle +1F376 ; fully-qualified # 🍶 E0.6 sake +1F37E ; fully-qualified # 🍾 E1.0 bottle with popping cork +1F377 ; fully-qualified # 🍷 E0.6 wine glass +1F378 ; fully-qualified # 🍸 E0.6 cocktail glass +1F379 ; fully-qualified # 🍹 E0.6 tropical drink +1F37A ; fully-qualified # 🍺 E0.6 beer mug +1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs +1F942 ; fully-qualified # 🥂 E3.0 clinking glasses +1F943 ; fully-qualified # 🥃 E3.0 tumbler glass +1F964 ; fully-qualified # 🥤 E5.0 cup with straw +1F9CB ; fully-qualified # 🧋 E13.0 bubble tea +1F9C3 ; fully-qualified # 🧃 E12.0 beverage box +1F9C9 ; fully-qualified # 🧉 E12.0 mate +1F9CA ; fully-qualified # 🧊 E12.0 ice + +# subgroup: dishware +1F962 ; fully-qualified # 🥢 E5.0 chopsticks +1F37D FE0F ; fully-qualified # 🍽️ E0.7 fork and knife with plate +1F37D ; unqualified # 🍽 E0.7 fork and knife with plate +1F374 ; fully-qualified # 🍴 E0.6 fork and knife +1F944 ; fully-qualified # 🥄 E3.0 spoon +1F52A ; fully-qualified # 🔪 E0.6 kitchen knife +1F3FA ; fully-qualified # 🏺 E1.0 amphora + +# Food & Drink subtotal: 131 +# Food & Drink subtotal: 131 w/o modifiers + +# group: Travel & Places + +# subgroup: place-map +1F30D ; fully-qualified # 🌍 E0.7 globe showing Europe-Africa +1F30E ; fully-qualified # 🌎 E0.7 globe showing Americas +1F30F ; fully-qualified # 🌏 E0.6 globe showing Asia-Australia +1F310 ; fully-qualified # 🌐 E1.0 globe with meridians +1F5FA FE0F ; fully-qualified # 🗺️ E0.7 world map +1F5FA ; unqualified # 🗺 E0.7 world map +1F5FE ; fully-qualified # 🗾 E0.6 map of Japan +1F9ED ; fully-qualified # 🧭 E11.0 compass + +# subgroup: place-geographic +1F3D4 FE0F ; fully-qualified # 🏔️ E0.7 snow-capped mountain +1F3D4 ; unqualified # 🏔 E0.7 snow-capped mountain +26F0 FE0F ; fully-qualified # ⛰️ E0.7 mountain +26F0 ; unqualified # ⛰ E0.7 mountain +1F30B ; fully-qualified # 🌋 E0.6 volcano +1F5FB ; fully-qualified # 🗻 E0.6 mount fuji +1F3D5 FE0F ; fully-qualified # 🏕️ E0.7 camping +1F3D5 ; unqualified # 🏕 E0.7 camping +1F3D6 FE0F ; fully-qualified # 🏖️ E0.7 beach with umbrella +1F3D6 ; unqualified # 🏖 E0.7 beach with umbrella +1F3DC FE0F ; fully-qualified # 🏜️ E0.7 desert +1F3DC ; unqualified # 🏜 E0.7 desert +1F3DD FE0F ; fully-qualified # 🏝️ E0.7 desert island +1F3DD ; unqualified # 🏝 E0.7 desert island +1F3DE FE0F ; fully-qualified # 🏞️ E0.7 national park +1F3DE ; unqualified # 🏞 E0.7 national park + +# subgroup: place-building +1F3DF FE0F ; fully-qualified # 🏟️ E0.7 stadium +1F3DF ; unqualified # 🏟 E0.7 stadium +1F3DB FE0F ; fully-qualified # 🏛️ E0.7 classical building +1F3DB ; unqualified # 🏛 E0.7 classical building +1F3D7 FE0F ; fully-qualified # 🏗️ E0.7 building construction +1F3D7 ; unqualified # 🏗 E0.7 building construction +1F9F1 ; fully-qualified # 🧱 E11.0 brick +1FAA8 ; fully-qualified # 🪨 E13.0 rock +1FAB5 ; fully-qualified # 🪵 E13.0 wood +1F6D6 ; fully-qualified # 🛖 E13.0 hut +1F3D8 FE0F ; fully-qualified # 🏘️ E0.7 houses +1F3D8 ; unqualified # 🏘 E0.7 houses +1F3DA FE0F ; fully-qualified # 🏚️ E0.7 derelict house +1F3DA ; unqualified # 🏚 E0.7 derelict house +1F3E0 ; fully-qualified # 🏠 E0.6 house +1F3E1 ; fully-qualified # 🏡 E0.6 house with garden +1F3E2 ; fully-qualified # 🏢 E0.6 office building +1F3E3 ; fully-qualified # 🏣 E0.6 Japanese post office +1F3E4 ; fully-qualified # 🏤 E1.0 post office +1F3E5 ; fully-qualified # 🏥 E0.6 hospital +1F3E6 ; fully-qualified # 🏦 E0.6 bank +1F3E8 ; fully-qualified # 🏨 E0.6 hotel +1F3E9 ; fully-qualified # 🏩 E0.6 love hotel +1F3EA ; fully-qualified # 🏪 E0.6 convenience store +1F3EB ; fully-qualified # 🏫 E0.6 school +1F3EC ; fully-qualified # 🏬 E0.6 department store +1F3ED ; fully-qualified # 🏭 E0.6 factory +1F3EF ; fully-qualified # 🏯 E0.6 Japanese castle +1F3F0 ; fully-qualified # 🏰 E0.6 castle +1F492 ; fully-qualified # 💒 E0.6 wedding +1F5FC ; fully-qualified # 🗼 E0.6 Tokyo tower +1F5FD ; fully-qualified # 🗽 E0.6 Statue of Liberty + +# subgroup: place-religious +26EA ; fully-qualified # ⛪ E0.6 church +1F54C ; fully-qualified # 🕌 E1.0 mosque +1F6D5 ; fully-qualified # 🛕 E12.0 hindu temple +1F54D ; fully-qualified # 🕍 E1.0 synagogue +26E9 FE0F ; fully-qualified # ⛩️ E0.7 shinto shrine +26E9 ; unqualified # ⛩ E0.7 shinto shrine +1F54B ; fully-qualified # 🕋 E1.0 kaaba + +# subgroup: place-other +26F2 ; fully-qualified # ⛲ E0.6 fountain +26FA ; fully-qualified # ⛺ E0.6 tent +1F301 ; fully-qualified # 🌁 E0.6 foggy +1F303 ; fully-qualified # 🌃 E0.6 night with stars +1F3D9 FE0F ; fully-qualified # 🏙️ E0.7 cityscape +1F3D9 ; unqualified # 🏙 E0.7 cityscape +1F304 ; fully-qualified # 🌄 E0.6 sunrise over mountains +1F305 ; fully-qualified # 🌅 E0.6 sunrise +1F306 ; fully-qualified # 🌆 E0.6 cityscape at dusk +1F307 ; fully-qualified # 🌇 E0.6 sunset +1F309 ; fully-qualified # 🌉 E0.6 bridge at night +2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs +2668 ; unqualified # ♨ E0.6 hot springs +1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse +1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel +1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster +1F488 ; fully-qualified # 💈 E0.6 barber pole +1F3AA ; fully-qualified # 🎪 E0.6 circus tent + +# subgroup: transport-ground +1F682 ; fully-qualified # 🚂 E1.0 locomotive +1F683 ; fully-qualified # 🚃 E0.6 railway car +1F684 ; fully-qualified # 🚄 E0.6 high-speed train +1F685 ; fully-qualified # 🚅 E0.6 bullet train +1F686 ; fully-qualified # 🚆 E1.0 train +1F687 ; fully-qualified # 🚇 E0.6 metro +1F688 ; fully-qualified # 🚈 E1.0 light rail +1F689 ; fully-qualified # 🚉 E0.6 station +1F68A ; fully-qualified # 🚊 E1.0 tram +1F69D ; fully-qualified # 🚝 E1.0 monorail +1F69E ; fully-qualified # 🚞 E1.0 mountain railway +1F68B ; fully-qualified # 🚋 E1.0 tram car +1F68C ; fully-qualified # 🚌 E0.6 bus +1F68D ; fully-qualified # 🚍 E0.7 oncoming bus +1F68E ; fully-qualified # 🚎 E1.0 trolleybus +1F690 ; fully-qualified # 🚐 E1.0 minibus +1F691 ; fully-qualified # 🚑 E0.6 ambulance +1F692 ; fully-qualified # 🚒 E0.6 fire engine +1F693 ; fully-qualified # 🚓 E0.6 police car +1F694 ; fully-qualified # 🚔 E0.7 oncoming police car +1F695 ; fully-qualified # 🚕 E0.6 taxi +1F696 ; fully-qualified # 🚖 E1.0 oncoming taxi +1F697 ; fully-qualified # 🚗 E0.6 automobile +1F698 ; fully-qualified # 🚘 E0.7 oncoming automobile +1F699 ; fully-qualified # 🚙 E0.6 sport utility vehicle +1F6FB ; fully-qualified # 🛻 E13.0 pickup truck +1F69A ; fully-qualified # 🚚 E0.6 delivery truck +1F69B ; fully-qualified # 🚛 E1.0 articulated lorry +1F69C ; fully-qualified # 🚜 E1.0 tractor +1F3CE FE0F ; fully-qualified # 🏎️ E0.7 racing car +1F3CE ; unqualified # 🏎 E0.7 racing car +1F3CD FE0F ; fully-qualified # 🏍️ E0.7 motorcycle +1F3CD ; unqualified # 🏍 E0.7 motorcycle +1F6F5 ; fully-qualified # 🛵 E3.0 motor scooter +1F9BD ; fully-qualified # 🦽 E12.0 manual wheelchair +1F9BC ; fully-qualified # 🦼 E12.0 motorized wheelchair +1F6FA ; fully-qualified # 🛺 E12.0 auto rickshaw +1F6B2 ; fully-qualified # 🚲 E0.6 bicycle +1F6F4 ; fully-qualified # 🛴 E3.0 kick scooter +1F6F9 ; fully-qualified # 🛹 E11.0 skateboard +1F6FC ; fully-qualified # 🛼 E13.0 roller skate +1F68F ; fully-qualified # 🚏 E0.6 bus stop +1F6E3 FE0F ; fully-qualified # 🛣️ E0.7 motorway +1F6E3 ; unqualified # 🛣 E0.7 motorway +1F6E4 FE0F ; fully-qualified # 🛤️ E0.7 railway track +1F6E4 ; unqualified # 🛤 E0.7 railway track +1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum +1F6E2 ; unqualified # 🛢 E0.7 oil drum +26FD ; fully-qualified # ⛽ E0.6 fuel pump +1F6A8 ; fully-qualified # 🚨 E0.6 police car light +1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light +1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light +1F6D1 ; fully-qualified # 🛑 E3.0 stop sign +1F6A7 ; fully-qualified # 🚧 E0.6 construction + +# subgroup: transport-water +2693 ; fully-qualified # ⚓ E0.6 anchor +26F5 ; fully-qualified # ⛵ E0.6 sailboat +1F6F6 ; fully-qualified # 🛶 E3.0 canoe +1F6A4 ; fully-qualified # 🚤 E0.6 speedboat +1F6F3 FE0F ; fully-qualified # 🛳️ E0.7 passenger ship +1F6F3 ; unqualified # 🛳 E0.7 passenger ship +26F4 FE0F ; fully-qualified # ⛴️ E0.7 ferry +26F4 ; unqualified # ⛴ E0.7 ferry +1F6E5 FE0F ; fully-qualified # 🛥️ E0.7 motor boat +1F6E5 ; unqualified # 🛥 E0.7 motor boat +1F6A2 ; fully-qualified # 🚢 E0.6 ship + +# subgroup: transport-air +2708 FE0F ; fully-qualified # ✈️ E0.6 airplane +2708 ; unqualified # ✈ E0.6 airplane +1F6E9 FE0F ; fully-qualified # 🛩️ E0.7 small airplane +1F6E9 ; unqualified # 🛩 E0.7 small airplane +1F6EB ; fully-qualified # 🛫 E1.0 airplane departure +1F6EC ; fully-qualified # 🛬 E1.0 airplane arrival +1FA82 ; fully-qualified # 🪂 E12.0 parachute +1F4BA ; fully-qualified # 💺 E0.6 seat +1F681 ; fully-qualified # 🚁 E1.0 helicopter +1F69F ; fully-qualified # 🚟 E1.0 suspension railway +1F6A0 ; fully-qualified # 🚠 E1.0 mountain cableway +1F6A1 ; fully-qualified # 🚡 E1.0 aerial tramway +1F6F0 FE0F ; fully-qualified # 🛰️ E0.7 satellite +1F6F0 ; unqualified # 🛰 E0.7 satellite +1F680 ; fully-qualified # 🚀 E0.6 rocket +1F6F8 ; fully-qualified # 🛸 E5.0 flying saucer + +# subgroup: hotel +1F6CE FE0F ; fully-qualified # 🛎️ E0.7 bellhop bell +1F6CE ; unqualified # 🛎 E0.7 bellhop bell +1F9F3 ; fully-qualified # 🧳 E11.0 luggage + +# subgroup: time +231B ; fully-qualified # ⌛ E0.6 hourglass done +23F3 ; fully-qualified # ⏳ E0.6 hourglass not done +231A ; fully-qualified # ⌚ E0.6 watch +23F0 ; fully-qualified # ⏰ E0.6 alarm clock +23F1 FE0F ; fully-qualified # ⏱️ E1.0 stopwatch +23F1 ; unqualified # ⏱ E1.0 stopwatch +23F2 FE0F ; fully-qualified # ⏲️ E1.0 timer clock +23F2 ; unqualified # ⏲ E1.0 timer clock +1F570 FE0F ; fully-qualified # 🕰️ E0.7 mantelpiece clock +1F570 ; unqualified # 🕰 E0.7 mantelpiece clock +1F55B ; fully-qualified # 🕛 E0.6 twelve o’clock +1F567 ; fully-qualified # 🕧 E0.7 twelve-thirty +1F550 ; fully-qualified # 🕐 E0.6 one o’clock +1F55C ; fully-qualified # 🕜 E0.7 one-thirty +1F551 ; fully-qualified # 🕑 E0.6 two o’clock +1F55D ; fully-qualified # 🕝 E0.7 two-thirty +1F552 ; fully-qualified # 🕒 E0.6 three o’clock +1F55E ; fully-qualified # 🕞 E0.7 three-thirty +1F553 ; fully-qualified # 🕓 E0.6 four o’clock +1F55F ; fully-qualified # 🕟 E0.7 four-thirty +1F554 ; fully-qualified # 🕔 E0.6 five o’clock +1F560 ; fully-qualified # 🕠 E0.7 five-thirty +1F555 ; fully-qualified # 🕕 E0.6 six o’clock +1F561 ; fully-qualified # 🕡 E0.7 six-thirty +1F556 ; fully-qualified # 🕖 E0.6 seven o’clock +1F562 ; fully-qualified # 🕢 E0.7 seven-thirty +1F557 ; fully-qualified # 🕗 E0.6 eight o’clock +1F563 ; fully-qualified # 🕣 E0.7 eight-thirty +1F558 ; fully-qualified # 🕘 E0.6 nine o’clock +1F564 ; fully-qualified # 🕤 E0.7 nine-thirty +1F559 ; fully-qualified # 🕙 E0.6 ten o’clock +1F565 ; fully-qualified # 🕥 E0.7 ten-thirty +1F55A ; fully-qualified # 🕚 E0.6 eleven o’clock +1F566 ; fully-qualified # 🕦 E0.7 eleven-thirty + +# subgroup: sky & weather +1F311 ; fully-qualified # 🌑 E0.6 new moon +1F312 ; fully-qualified # 🌒 E1.0 waxing crescent moon +1F313 ; fully-qualified # 🌓 E0.6 first quarter moon +1F314 ; fully-qualified # 🌔 E0.6 waxing gibbous moon +1F315 ; fully-qualified # 🌕 E0.6 full moon +1F316 ; fully-qualified # 🌖 E1.0 waning gibbous moon +1F317 ; fully-qualified # 🌗 E1.0 last quarter moon +1F318 ; fully-qualified # 🌘 E1.0 waning crescent moon +1F319 ; fully-qualified # 🌙 E0.6 crescent moon +1F31A ; fully-qualified # 🌚 E1.0 new moon face +1F31B ; fully-qualified # 🌛 E0.6 first quarter moon face +1F31C ; fully-qualified # 🌜 E0.7 last quarter moon face +1F321 FE0F ; fully-qualified # 🌡️ E0.7 thermometer +1F321 ; unqualified # 🌡 E0.7 thermometer +2600 FE0F ; fully-qualified # ☀️ E0.6 sun +2600 ; unqualified # ☀ E0.6 sun +1F31D ; fully-qualified # 🌝 E1.0 full moon face +1F31E ; fully-qualified # 🌞 E1.0 sun with face +1FA90 ; fully-qualified # 🪐 E12.0 ringed planet +2B50 ; fully-qualified # ⭐ E0.6 star +1F31F ; fully-qualified # 🌟 E0.6 glowing star +1F320 ; fully-qualified # 🌠 E0.6 shooting star +1F30C ; fully-qualified # 🌌 E0.6 milky way +2601 FE0F ; fully-qualified # ☁️ E0.6 cloud +2601 ; unqualified # ☁ E0.6 cloud +26C5 ; fully-qualified # ⛅ E0.6 sun behind cloud +26C8 FE0F ; fully-qualified # ⛈️ E0.7 cloud with lightning and rain +26C8 ; unqualified # ⛈ E0.7 cloud with lightning and rain +1F324 FE0F ; fully-qualified # 🌤️ E0.7 sun behind small cloud +1F324 ; unqualified # 🌤 E0.7 sun behind small cloud +1F325 FE0F ; fully-qualified # 🌥️ E0.7 sun behind large cloud +1F325 ; unqualified # 🌥 E0.7 sun behind large cloud +1F326 FE0F ; fully-qualified # 🌦️ E0.7 sun behind rain cloud +1F326 ; unqualified # 🌦 E0.7 sun behind rain cloud +1F327 FE0F ; fully-qualified # 🌧️ E0.7 cloud with rain +1F327 ; unqualified # 🌧 E0.7 cloud with rain +1F328 FE0F ; fully-qualified # 🌨️ E0.7 cloud with snow +1F328 ; unqualified # 🌨 E0.7 cloud with snow +1F329 FE0F ; fully-qualified # 🌩️ E0.7 cloud with lightning +1F329 ; unqualified # 🌩 E0.7 cloud with lightning +1F32A FE0F ; fully-qualified # 🌪️ E0.7 tornado +1F32A ; unqualified # 🌪 E0.7 tornado +1F32B FE0F ; fully-qualified # 🌫️ E0.7 fog +1F32B ; unqualified # 🌫 E0.7 fog +1F32C FE0F ; fully-qualified # 🌬️ E0.7 wind face +1F32C ; unqualified # 🌬 E0.7 wind face +1F300 ; fully-qualified # 🌀 E0.6 cyclone +1F308 ; fully-qualified # 🌈 E0.6 rainbow +1F302 ; fully-qualified # 🌂 E0.6 closed umbrella +2602 FE0F ; fully-qualified # ☂️ E0.7 umbrella +2602 ; unqualified # ☂ E0.7 umbrella +2614 ; fully-qualified # ☔ E0.6 umbrella with rain drops +26F1 FE0F ; fully-qualified # ⛱️ E0.7 umbrella on ground +26F1 ; unqualified # ⛱ E0.7 umbrella on ground +26A1 ; fully-qualified # ⚡ E0.6 high voltage +2744 FE0F ; fully-qualified # ❄️ E0.6 snowflake +2744 ; unqualified # ❄ E0.6 snowflake +2603 FE0F ; fully-qualified # ☃️ E0.7 snowman +2603 ; unqualified # ☃ E0.7 snowman +26C4 ; fully-qualified # ⛄ E0.6 snowman without snow +2604 FE0F ; fully-qualified # ☄️ E1.0 comet +2604 ; unqualified # ☄ E1.0 comet +1F525 ; fully-qualified # 🔥 E0.6 fire +1F4A7 ; fully-qualified # 💧 E0.6 droplet +1F30A ; fully-qualified # 🌊 E0.6 water wave + +# Travel & Places subtotal: 264 +# Travel & Places subtotal: 264 w/o modifiers + +# group: Activities + +# subgroup: event +1F383 ; fully-qualified # 🎃 E0.6 jack-o-lantern +1F384 ; fully-qualified # 🎄 E0.6 Christmas tree +1F386 ; fully-qualified # 🎆 E0.6 fireworks +1F387 ; fully-qualified # 🎇 E0.6 sparkler +1F9E8 ; fully-qualified # 🧨 E11.0 firecracker +2728 ; fully-qualified # ✨ E0.6 sparkles +1F388 ; fully-qualified # 🎈 E0.6 balloon +1F389 ; fully-qualified # 🎉 E0.6 party popper +1F38A ; fully-qualified # 🎊 E0.6 confetti ball +1F38B ; fully-qualified # 🎋 E0.6 tanabata tree +1F38D ; fully-qualified # 🎍 E0.6 pine decoration +1F38E ; fully-qualified # 🎎 E0.6 Japanese dolls +1F38F ; fully-qualified # 🎏 E0.6 carp streamer +1F390 ; fully-qualified # 🎐 E0.6 wind chime +1F391 ; fully-qualified # 🎑 E0.6 moon viewing ceremony +1F9E7 ; fully-qualified # 🧧 E11.0 red envelope +1F380 ; fully-qualified # 🎀 E0.6 ribbon +1F381 ; fully-qualified # 🎁 E0.6 wrapped gift +1F397 FE0F ; fully-qualified # 🎗️ E0.7 reminder ribbon +1F397 ; unqualified # 🎗 E0.7 reminder ribbon +1F39F FE0F ; fully-qualified # 🎟️ E0.7 admission tickets +1F39F ; unqualified # 🎟 E0.7 admission tickets +1F3AB ; fully-qualified # 🎫 E0.6 ticket + +# subgroup: award-medal +1F396 FE0F ; fully-qualified # 🎖️ E0.7 military medal +1F396 ; unqualified # 🎖 E0.7 military medal +1F3C6 ; fully-qualified # 🏆 E0.6 trophy +1F3C5 ; fully-qualified # 🏅 E1.0 sports medal +1F947 ; fully-qualified # 🥇 E3.0 1st place medal +1F948 ; fully-qualified # 🥈 E3.0 2nd place medal +1F949 ; fully-qualified # 🥉 E3.0 3rd place medal + +# subgroup: sport +26BD ; fully-qualified # ⚽ E0.6 soccer ball +26BE ; fully-qualified # ⚾ E0.6 baseball +1F94E ; fully-qualified # 🥎 E11.0 softball +1F3C0 ; fully-qualified # 🏀 E0.6 basketball +1F3D0 ; fully-qualified # 🏐 E1.0 volleyball +1F3C8 ; fully-qualified # 🏈 E0.6 american football +1F3C9 ; fully-qualified # 🏉 E1.0 rugby football +1F3BE ; fully-qualified # 🎾 E0.6 tennis +1F94F ; fully-qualified # 🥏 E11.0 flying disc +1F3B3 ; fully-qualified # 🎳 E0.6 bowling +1F3CF ; fully-qualified # 🏏 E1.0 cricket game +1F3D1 ; fully-qualified # 🏑 E1.0 field hockey +1F3D2 ; fully-qualified # 🏒 E1.0 ice hockey +1F94D ; fully-qualified # 🥍 E11.0 lacrosse +1F3D3 ; fully-qualified # 🏓 E1.0 ping pong +1F3F8 ; fully-qualified # 🏸 E1.0 badminton +1F94A ; fully-qualified # 🥊 E3.0 boxing glove +1F94B ; fully-qualified # 🥋 E3.0 martial arts uniform +1F945 ; fully-qualified # 🥅 E3.0 goal net +26F3 ; fully-qualified # ⛳ E0.6 flag in hole +26F8 FE0F ; fully-qualified # ⛸️ E0.7 ice skate +26F8 ; unqualified # ⛸ E0.7 ice skate +1F3A3 ; fully-qualified # 🎣 E0.6 fishing pole +1F93F ; fully-qualified # 🤿 E12.0 diving mask +1F3BD ; fully-qualified # 🎽 E0.6 running shirt +1F3BF ; fully-qualified # 🎿 E0.6 skis +1F6F7 ; fully-qualified # 🛷 E5.0 sled +1F94C ; fully-qualified # 🥌 E5.0 curling stone + +# subgroup: game +1F3AF ; fully-qualified # 🎯 E0.6 direct hit +1FA80 ; fully-qualified # 🪀 E12.0 yo-yo +1FA81 ; fully-qualified # 🪁 E12.0 kite +1F3B1 ; fully-qualified # 🎱 E0.6 pool 8 ball +1F52E ; fully-qualified # 🔮 E0.6 crystal ball +1FA84 ; fully-qualified # 🪄 E13.0 magic wand +1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet +1F3AE ; fully-qualified # 🎮 E0.6 video game +1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick +1F579 ; unqualified # 🕹 E0.7 joystick +1F3B0 ; fully-qualified # 🎰 E0.6 slot machine +1F3B2 ; fully-qualified # 🎲 E0.6 game die +1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece +1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear +1FA85 ; fully-qualified # 🪅 E13.0 piñata +1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls +2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit +2660 ; unqualified # ♠ E0.6 spade suit +2665 FE0F ; fully-qualified # ♥️ E0.6 heart suit +2665 ; unqualified # ♥ E0.6 heart suit +2666 FE0F ; fully-qualified # ♦️ E0.6 diamond suit +2666 ; unqualified # ♦ E0.6 diamond suit +2663 FE0F ; fully-qualified # ♣️ E0.6 club suit +2663 ; unqualified # ♣ E0.6 club suit +265F FE0F ; fully-qualified # ♟️ E11.0 chess pawn +265F ; unqualified # ♟ E11.0 chess pawn +1F0CF ; fully-qualified # 🃏 E0.6 joker +1F004 ; fully-qualified # 🀄 E0.6 mahjong red dragon +1F3B4 ; fully-qualified # 🎴 E0.6 flower playing cards + +# subgroup: arts & crafts +1F3AD ; fully-qualified # 🎭 E0.6 performing arts +1F5BC FE0F ; fully-qualified # 🖼️ E0.7 framed picture +1F5BC ; unqualified # 🖼 E0.7 framed picture +1F3A8 ; fully-qualified # 🎨 E0.6 artist palette +1F9F5 ; fully-qualified # 🧵 E11.0 thread +1FAA1 ; fully-qualified # 🪡 E13.0 sewing needle +1F9F6 ; fully-qualified # 🧶 E11.0 yarn +1FAA2 ; fully-qualified # 🪢 E13.0 knot + +# Activities subtotal: 95 +# Activities subtotal: 95 w/o modifiers + +# group: Objects + +# subgroup: clothing +1F453 ; fully-qualified # 👓 E0.6 glasses +1F576 FE0F ; fully-qualified # 🕶️ E0.7 sunglasses +1F576 ; unqualified # 🕶 E0.7 sunglasses +1F97D ; fully-qualified # 🥽 E11.0 goggles +1F97C ; fully-qualified # 🥼 E11.0 lab coat +1F9BA ; fully-qualified # 🦺 E12.0 safety vest +1F454 ; fully-qualified # 👔 E0.6 necktie +1F455 ; fully-qualified # 👕 E0.6 t-shirt +1F456 ; fully-qualified # 👖 E0.6 jeans +1F9E3 ; fully-qualified # 🧣 E5.0 scarf +1F9E4 ; fully-qualified # 🧤 E5.0 gloves +1F9E5 ; fully-qualified # 🧥 E5.0 coat +1F9E6 ; fully-qualified # 🧦 E5.0 socks +1F457 ; fully-qualified # 👗 E0.6 dress +1F458 ; fully-qualified # 👘 E0.6 kimono +1F97B ; fully-qualified # 🥻 E12.0 sari +1FA71 ; fully-qualified # 🩱 E12.0 one-piece swimsuit +1FA72 ; fully-qualified # 🩲 E12.0 briefs +1FA73 ; fully-qualified # 🩳 E12.0 shorts +1F459 ; fully-qualified # 👙 E0.6 bikini +1F45A ; fully-qualified # 👚 E0.6 woman’s clothes +1F45B ; fully-qualified # 👛 E0.6 purse +1F45C ; fully-qualified # 👜 E0.6 handbag +1F45D ; fully-qualified # 👝 E0.6 clutch bag +1F6CD FE0F ; fully-qualified # 🛍️ E0.7 shopping bags +1F6CD ; unqualified # 🛍 E0.7 shopping bags +1F392 ; fully-qualified # 🎒 E0.6 backpack +1FA74 ; fully-qualified # 🩴 E13.0 thong sandal +1F45E ; fully-qualified # 👞 E0.6 man’s shoe +1F45F ; fully-qualified # 👟 E0.6 running shoe +1F97E ; fully-qualified # 🥾 E11.0 hiking boot +1F97F ; fully-qualified # 🥿 E11.0 flat shoe +1F460 ; fully-qualified # 👠 E0.6 high-heeled shoe +1F461 ; fully-qualified # 👡 E0.6 woman’s sandal +1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes +1F462 ; fully-qualified # 👢 E0.6 woman’s boot +1F451 ; fully-qualified # 👑 E0.6 crown +1F452 ; fully-qualified # 👒 E0.6 woman’s hat +1F3A9 ; fully-qualified # 🎩 E0.6 top hat +1F393 ; fully-qualified # 🎓 E0.6 graduation cap +1F9E2 ; fully-qualified # 🧢 E5.0 billed cap +1FA96 ; fully-qualified # 🪖 E13.0 military helmet +26D1 FE0F ; fully-qualified # ⛑️ E0.7 rescue worker’s helmet +26D1 ; unqualified # ⛑ E0.7 rescue worker’s helmet +1F4FF ; fully-qualified # 📿 E1.0 prayer beads +1F484 ; fully-qualified # 💄 E0.6 lipstick +1F48D ; fully-qualified # 💍 E0.6 ring +1F48E ; fully-qualified # 💎 E0.6 gem stone + +# subgroup: sound +1F507 ; fully-qualified # 🔇 E1.0 muted speaker +1F508 ; fully-qualified # 🔈 E0.7 speaker low volume +1F509 ; fully-qualified # 🔉 E1.0 speaker medium volume +1F50A ; fully-qualified # 🔊 E0.6 speaker high volume +1F4E2 ; fully-qualified # 📢 E0.6 loudspeaker +1F4E3 ; fully-qualified # 📣 E0.6 megaphone +1F4EF ; fully-qualified # 📯 E1.0 postal horn +1F514 ; fully-qualified # 🔔 E0.6 bell +1F515 ; fully-qualified # 🔕 E1.0 bell with slash + +# subgroup: music +1F3BC ; fully-qualified # 🎼 E0.6 musical score +1F3B5 ; fully-qualified # 🎵 E0.6 musical note +1F3B6 ; fully-qualified # 🎶 E0.6 musical notes +1F399 FE0F ; fully-qualified # 🎙️ E0.7 studio microphone +1F399 ; unqualified # 🎙 E0.7 studio microphone +1F39A FE0F ; fully-qualified # 🎚️ E0.7 level slider +1F39A ; unqualified # 🎚 E0.7 level slider +1F39B FE0F ; fully-qualified # 🎛️ E0.7 control knobs +1F39B ; unqualified # 🎛 E0.7 control knobs +1F3A4 ; fully-qualified # 🎤 E0.6 microphone +1F3A7 ; fully-qualified # 🎧 E0.6 headphone +1F4FB ; fully-qualified # 📻 E0.6 radio + +# subgroup: musical-instrument +1F3B7 ; fully-qualified # 🎷 E0.6 saxophone +1FA97 ; fully-qualified # 🪗 E13.0 accordion +1F3B8 ; fully-qualified # 🎸 E0.6 guitar +1F3B9 ; fully-qualified # 🎹 E0.6 musical keyboard +1F3BA ; fully-qualified # 🎺 E0.6 trumpet +1F3BB ; fully-qualified # 🎻 E0.6 violin +1FA95 ; fully-qualified # 🪕 E12.0 banjo +1F941 ; fully-qualified # 🥁 E3.0 drum +1FA98 ; fully-qualified # 🪘 E13.0 long drum + +# subgroup: phone +1F4F1 ; fully-qualified # 📱 E0.6 mobile phone +1F4F2 ; fully-qualified # 📲 E0.6 mobile phone with arrow +260E FE0F ; fully-qualified # ☎️ E0.6 telephone +260E ; unqualified # ☎ E0.6 telephone +1F4DE ; fully-qualified # 📞 E0.6 telephone receiver +1F4DF ; fully-qualified # 📟 E0.6 pager +1F4E0 ; fully-qualified # 📠 E0.6 fax machine + +# subgroup: computer +1F50B ; fully-qualified # 🔋 E0.6 battery +1F50C ; fully-qualified # 🔌 E0.6 electric plug +1F4BB ; fully-qualified # 💻 E0.6 laptop +1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer +1F5A5 ; unqualified # 🖥 E0.7 desktop computer +1F5A8 FE0F ; fully-qualified # 🖨️ E0.7 printer +1F5A8 ; unqualified # 🖨 E0.7 printer +2328 FE0F ; fully-qualified # ⌨️ E1.0 keyboard +2328 ; unqualified # ⌨ E1.0 keyboard +1F5B1 FE0F ; fully-qualified # 🖱️ E0.7 computer mouse +1F5B1 ; unqualified # 🖱 E0.7 computer mouse +1F5B2 FE0F ; fully-qualified # 🖲️ E0.7 trackball +1F5B2 ; unqualified # 🖲 E0.7 trackball +1F4BD ; fully-qualified # 💽 E0.6 computer disk +1F4BE ; fully-qualified # 💾 E0.6 floppy disk +1F4BF ; fully-qualified # 💿 E0.6 optical disk +1F4C0 ; fully-qualified # 📀 E0.6 dvd +1F9EE ; fully-qualified # 🧮 E11.0 abacus + +# subgroup: light & video +1F3A5 ; fully-qualified # 🎥 E0.6 movie camera +1F39E FE0F ; fully-qualified # 🎞️ E0.7 film frames +1F39E ; unqualified # 🎞 E0.7 film frames +1F4FD FE0F ; fully-qualified # 📽️ E0.7 film projector +1F4FD ; unqualified # 📽 E0.7 film projector +1F3AC ; fully-qualified # 🎬 E0.6 clapper board +1F4FA ; fully-qualified # 📺 E0.6 television +1F4F7 ; fully-qualified # 📷 E0.6 camera +1F4F8 ; fully-qualified # 📸 E1.0 camera with flash +1F4F9 ; fully-qualified # 📹 E0.6 video camera +1F4FC ; fully-qualified # 📼 E0.6 videocassette +1F50D ; fully-qualified # 🔍 E0.6 magnifying glass tilted left +1F50E ; fully-qualified # 🔎 E0.6 magnifying glass tilted right +1F56F FE0F ; fully-qualified # 🕯️ E0.7 candle +1F56F ; unqualified # 🕯 E0.7 candle +1F4A1 ; fully-qualified # 💡 E0.6 light bulb +1F526 ; fully-qualified # 🔦 E0.6 flashlight +1F3EE ; fully-qualified # 🏮 E0.6 red paper lantern +1FA94 ; fully-qualified # 🪔 E12.0 diya lamp + +# subgroup: book-paper +1F4D4 ; fully-qualified # 📔 E0.6 notebook with decorative cover +1F4D5 ; fully-qualified # 📕 E0.6 closed book +1F4D6 ; fully-qualified # 📖 E0.6 open book +1F4D7 ; fully-qualified # 📗 E0.6 green book +1F4D8 ; fully-qualified # 📘 E0.6 blue book +1F4D9 ; fully-qualified # 📙 E0.6 orange book +1F4DA ; fully-qualified # 📚 E0.6 books +1F4D3 ; fully-qualified # 📓 E0.6 notebook +1F4D2 ; fully-qualified # 📒 E0.6 ledger +1F4C3 ; fully-qualified # 📃 E0.6 page with curl +1F4DC ; fully-qualified # 📜 E0.6 scroll +1F4C4 ; fully-qualified # 📄 E0.6 page facing up +1F4F0 ; fully-qualified # 📰 E0.6 newspaper +1F5DE FE0F ; fully-qualified # 🗞️ E0.7 rolled-up newspaper +1F5DE ; unqualified # 🗞 E0.7 rolled-up newspaper +1F4D1 ; fully-qualified # 📑 E0.6 bookmark tabs +1F516 ; fully-qualified # 🔖 E0.6 bookmark +1F3F7 FE0F ; fully-qualified # 🏷️ E0.7 label +1F3F7 ; unqualified # 🏷 E0.7 label + +# subgroup: money +1F4B0 ; fully-qualified # 💰 E0.6 money bag +1FA99 ; fully-qualified # 🪙 E13.0 coin +1F4B4 ; fully-qualified # 💴 E0.6 yen banknote +1F4B5 ; fully-qualified # 💵 E0.6 dollar banknote +1F4B6 ; fully-qualified # 💶 E1.0 euro banknote +1F4B7 ; fully-qualified # 💷 E1.0 pound banknote +1F4B8 ; fully-qualified # 💸 E0.6 money with wings +1F4B3 ; fully-qualified # 💳 E0.6 credit card +1F9FE ; fully-qualified # 🧾 E11.0 receipt +1F4B9 ; fully-qualified # 💹 E0.6 chart increasing with yen +1F4B1 ; fully-qualified # 💱 E0.6 currency exchange +1F4B2 ; fully-qualified # 💲 E0.6 heavy dollar sign + +# subgroup: mail +2709 FE0F ; fully-qualified # ✉️ E0.6 envelope +2709 ; unqualified # ✉ E0.6 envelope +1F4E7 ; fully-qualified # 📧 E0.6 e-mail +1F4E8 ; fully-qualified # 📨 E0.6 incoming envelope +1F4E9 ; fully-qualified # 📩 E0.6 envelope with arrow +1F4E4 ; fully-qualified # 📤 E0.6 outbox tray +1F4E5 ; fully-qualified # 📥 E0.6 inbox tray +1F4E6 ; fully-qualified # 📦 E0.6 package +1F4EB ; fully-qualified # 📫 E0.6 closed mailbox with raised flag +1F4EA ; fully-qualified # 📪 E0.6 closed mailbox with lowered flag +1F4EC ; fully-qualified # 📬 E0.7 open mailbox with raised flag +1F4ED ; fully-qualified # 📭 E0.7 open mailbox with lowered flag +1F4EE ; fully-qualified # 📮 E0.6 postbox +1F5F3 FE0F ; fully-qualified # 🗳️ E0.7 ballot box with ballot +1F5F3 ; unqualified # 🗳 E0.7 ballot box with ballot + +# subgroup: writing +270F FE0F ; fully-qualified # ✏️ E0.6 pencil +270F ; unqualified # ✏ E0.6 pencil +2712 FE0F ; fully-qualified # ✒️ E0.6 black nib +2712 ; unqualified # ✒ E0.6 black nib +1F58B FE0F ; fully-qualified # 🖋️ E0.7 fountain pen +1F58B ; unqualified # 🖋 E0.7 fountain pen +1F58A FE0F ; fully-qualified # 🖊️ E0.7 pen +1F58A ; unqualified # 🖊 E0.7 pen +1F58C FE0F ; fully-qualified # 🖌️ E0.7 paintbrush +1F58C ; unqualified # 🖌 E0.7 paintbrush +1F58D FE0F ; fully-qualified # 🖍️ E0.7 crayon +1F58D ; unqualified # 🖍 E0.7 crayon +1F4DD ; fully-qualified # 📝 E0.6 memo + +# subgroup: office +1F4BC ; fully-qualified # 💼 E0.6 briefcase +1F4C1 ; fully-qualified # 📁 E0.6 file folder +1F4C2 ; fully-qualified # 📂 E0.6 open file folder +1F5C2 FE0F ; fully-qualified # 🗂️ E0.7 card index dividers +1F5C2 ; unqualified # 🗂 E0.7 card index dividers +1F4C5 ; fully-qualified # 📅 E0.6 calendar +1F4C6 ; fully-qualified # 📆 E0.6 tear-off calendar +1F5D2 FE0F ; fully-qualified # 🗒️ E0.7 spiral notepad +1F5D2 ; unqualified # 🗒 E0.7 spiral notepad +1F5D3 FE0F ; fully-qualified # 🗓️ E0.7 spiral calendar +1F5D3 ; unqualified # 🗓 E0.7 spiral calendar +1F4C7 ; fully-qualified # 📇 E0.6 card index +1F4C8 ; fully-qualified # 📈 E0.6 chart increasing +1F4C9 ; fully-qualified # 📉 E0.6 chart decreasing +1F4CA ; fully-qualified # 📊 E0.6 bar chart +1F4CB ; fully-qualified # 📋 E0.6 clipboard +1F4CC ; fully-qualified # 📌 E0.6 pushpin +1F4CD ; fully-qualified # 📍 E0.6 round pushpin +1F4CE ; fully-qualified # 📎 E0.6 paperclip +1F587 FE0F ; fully-qualified # 🖇️ E0.7 linked paperclips +1F587 ; unqualified # 🖇 E0.7 linked paperclips +1F4CF ; fully-qualified # 📏 E0.6 straight ruler +1F4D0 ; fully-qualified # 📐 E0.6 triangular ruler +2702 FE0F ; fully-qualified # ✂️ E0.6 scissors +2702 ; unqualified # ✂ E0.6 scissors +1F5C3 FE0F ; fully-qualified # 🗃️ E0.7 card file box +1F5C3 ; unqualified # 🗃 E0.7 card file box +1F5C4 FE0F ; fully-qualified # 🗄️ E0.7 file cabinet +1F5C4 ; unqualified # 🗄 E0.7 file cabinet +1F5D1 FE0F ; fully-qualified # 🗑️ E0.7 wastebasket +1F5D1 ; unqualified # 🗑 E0.7 wastebasket + +# subgroup: lock +1F512 ; fully-qualified # 🔒 E0.6 locked +1F513 ; fully-qualified # 🔓 E0.6 unlocked +1F50F ; fully-qualified # 🔏 E0.6 locked with pen +1F510 ; fully-qualified # 🔐 E0.6 locked with key +1F511 ; fully-qualified # 🔑 E0.6 key +1F5DD FE0F ; fully-qualified # 🗝️ E0.7 old key +1F5DD ; unqualified # 🗝 E0.7 old key + +# subgroup: tool +1F528 ; fully-qualified # 🔨 E0.6 hammer +1FA93 ; fully-qualified # 🪓 E12.0 axe +26CF FE0F ; fully-qualified # ⛏️ E0.7 pick +26CF ; unqualified # ⛏ E0.7 pick +2692 FE0F ; fully-qualified # ⚒️ E1.0 hammer and pick +2692 ; unqualified # ⚒ E1.0 hammer and pick +1F6E0 FE0F ; fully-qualified # 🛠️ E0.7 hammer and wrench +1F6E0 ; unqualified # 🛠 E0.7 hammer and wrench +1F5E1 FE0F ; fully-qualified # 🗡️ E0.7 dagger +1F5E1 ; unqualified # 🗡 E0.7 dagger +2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords +2694 ; unqualified # ⚔ E1.0 crossed swords +1F52B ; fully-qualified # 🔫 E0.6 pistol +1FA83 ; fully-qualified # 🪃 E13.0 boomerang +1F3F9 ; fully-qualified # 🏹 E1.0 bow and arrow +1F6E1 FE0F ; fully-qualified # 🛡️ E0.7 shield +1F6E1 ; unqualified # 🛡 E0.7 shield +1FA9A ; fully-qualified # 🪚 E13.0 carpentry saw +1F527 ; fully-qualified # 🔧 E0.6 wrench +1FA9B ; fully-qualified # 🪛 E13.0 screwdriver +1F529 ; fully-qualified # 🔩 E0.6 nut and bolt +2699 FE0F ; fully-qualified # ⚙️ E1.0 gear +2699 ; unqualified # ⚙ E1.0 gear +1F5DC FE0F ; fully-qualified # 🗜️ E0.7 clamp +1F5DC ; unqualified # 🗜 E0.7 clamp +2696 FE0F ; fully-qualified # ⚖️ E1.0 balance scale +2696 ; unqualified # ⚖ E1.0 balance scale +1F9AF ; fully-qualified # 🦯 E12.0 probing cane +1F517 ; fully-qualified # 🔗 E0.6 link +26D3 FE0F ; fully-qualified # ⛓️ E0.7 chains +26D3 ; unqualified # ⛓ E0.7 chains +1FA9D ; fully-qualified # 🪝 E13.0 hook +1F9F0 ; fully-qualified # 🧰 E11.0 toolbox +1F9F2 ; fully-qualified # 🧲 E11.0 magnet +1FA9C ; fully-qualified # 🪜 E13.0 ladder + +# subgroup: science +2697 FE0F ; fully-qualified # ⚗️ E1.0 alembic +2697 ; unqualified # ⚗ E1.0 alembic +1F9EA ; fully-qualified # 🧪 E11.0 test tube +1F9EB ; fully-qualified # 🧫 E11.0 petri dish +1F9EC ; fully-qualified # 🧬 E11.0 dna +1F52C ; fully-qualified # 🔬 E1.0 microscope +1F52D ; fully-qualified # 🔭 E1.0 telescope +1F4E1 ; fully-qualified # 📡 E0.6 satellite antenna + +# subgroup: medical +1F489 ; fully-qualified # 💉 E0.6 syringe +1FA78 ; fully-qualified # 🩸 E12.0 drop of blood +1F48A ; fully-qualified # 💊 E0.6 pill +1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage +1FA7A ; fully-qualified # 🩺 E12.0 stethoscope + +# subgroup: household +1F6AA ; fully-qualified # 🚪 E0.6 door +1F6D7 ; fully-qualified # 🛗 E13.0 elevator +1FA9E ; fully-qualified # 🪞 E13.0 mirror +1FA9F ; fully-qualified # 🪟 E13.0 window +1F6CF FE0F ; fully-qualified # 🛏️ E0.7 bed +1F6CF ; unqualified # 🛏 E0.7 bed +1F6CB FE0F ; fully-qualified # 🛋️ E0.7 couch and lamp +1F6CB ; unqualified # 🛋 E0.7 couch and lamp +1FA91 ; fully-qualified # 🪑 E12.0 chair +1F6BD ; fully-qualified # 🚽 E0.6 toilet +1FAA0 ; fully-qualified # 🪠 E13.0 plunger +1F6BF ; fully-qualified # 🚿 E1.0 shower +1F6C1 ; fully-qualified # 🛁 E1.0 bathtub +1FAA4 ; fully-qualified # 🪤 E13.0 mouse trap +1FA92 ; fully-qualified # 🪒 E12.0 razor +1F9F4 ; fully-qualified # 🧴 E11.0 lotion bottle +1F9F7 ; fully-qualified # 🧷 E11.0 safety pin +1F9F9 ; fully-qualified # 🧹 E11.0 broom +1F9FA ; fully-qualified # 🧺 E11.0 basket +1F9FB ; fully-qualified # 🧻 E11.0 roll of paper +1FAA3 ; fully-qualified # 🪣 E13.0 bucket +1F9FC ; fully-qualified # 🧼 E11.0 soap +1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush +1F9FD ; fully-qualified # 🧽 E11.0 sponge +1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher +1F6D2 ; fully-qualified # 🛒 E3.0 shopping cart + +# subgroup: other-object +1F6AC ; fully-qualified # 🚬 E0.6 cigarette +26B0 FE0F ; fully-qualified # ⚰️ E1.0 coffin +26B0 ; unqualified # ⚰ E1.0 coffin +1FAA6 ; fully-qualified # 🪦 E13.0 headstone +26B1 FE0F ; fully-qualified # ⚱️ E1.0 funeral urn +26B1 ; unqualified # ⚱ E1.0 funeral urn +1F5FF ; fully-qualified # 🗿 E0.6 moai +1FAA7 ; fully-qualified # 🪧 E13.0 placard + +# Objects subtotal: 301 +# Objects subtotal: 301 w/o modifiers + +# group: Symbols + +# subgroup: transport-sign +1F3E7 ; fully-qualified # 🏧 E0.6 ATM sign +1F6AE ; fully-qualified # 🚮 E1.0 litter in bin sign +1F6B0 ; fully-qualified # 🚰 E1.0 potable water +267F ; fully-qualified # ♿ E0.6 wheelchair symbol +1F6B9 ; fully-qualified # 🚹 E0.6 men’s room +1F6BA ; fully-qualified # 🚺 E0.6 women’s room +1F6BB ; fully-qualified # 🚻 E0.6 restroom +1F6BC ; fully-qualified # 🚼 E0.6 baby symbol +1F6BE ; fully-qualified # 🚾 E0.6 water closet +1F6C2 ; fully-qualified # 🛂 E1.0 passport control +1F6C3 ; fully-qualified # 🛃 E1.0 customs +1F6C4 ; fully-qualified # 🛄 E1.0 baggage claim +1F6C5 ; fully-qualified # 🛅 E1.0 left luggage + +# subgroup: warning +26A0 FE0F ; fully-qualified # ⚠️ E0.6 warning +26A0 ; unqualified # ⚠ E0.6 warning +1F6B8 ; fully-qualified # 🚸 E1.0 children crossing +26D4 ; fully-qualified # ⛔ E0.6 no entry +1F6AB ; fully-qualified # 🚫 E0.6 prohibited +1F6B3 ; fully-qualified # 🚳 E1.0 no bicycles +1F6AD ; fully-qualified # 🚭 E0.6 no smoking +1F6AF ; fully-qualified # 🚯 E1.0 no littering +1F6B1 ; fully-qualified # 🚱 E1.0 non-potable water +1F6B7 ; fully-qualified # 🚷 E1.0 no pedestrians +1F4F5 ; fully-qualified # 📵 E1.0 no mobile phones +1F51E ; fully-qualified # 🔞 E0.6 no one under eighteen +2622 FE0F ; fully-qualified # ☢️ E1.0 radioactive +2622 ; unqualified # ☢ E1.0 radioactive +2623 FE0F ; fully-qualified # ☣️ E1.0 biohazard +2623 ; unqualified # ☣ E1.0 biohazard + +# subgroup: arrow +2B06 FE0F ; fully-qualified # ⬆️ E0.6 up arrow +2B06 ; unqualified # ⬆ E0.6 up arrow +2197 FE0F ; fully-qualified # ↗️ E0.6 up-right arrow +2197 ; unqualified # ↗ E0.6 up-right arrow +27A1 FE0F ; fully-qualified # ➡️ E0.6 right arrow +27A1 ; unqualified # ➡ E0.6 right arrow +2198 FE0F ; fully-qualified # ↘️ E0.6 down-right arrow +2198 ; unqualified # ↘ E0.6 down-right arrow +2B07 FE0F ; fully-qualified # ⬇️ E0.6 down arrow +2B07 ; unqualified # ⬇ E0.6 down arrow +2199 FE0F ; fully-qualified # ↙️ E0.6 down-left arrow +2199 ; unqualified # ↙ E0.6 down-left arrow +2B05 FE0F ; fully-qualified # ⬅️ E0.6 left arrow +2B05 ; unqualified # ⬅ E0.6 left arrow +2196 FE0F ; fully-qualified # ↖️ E0.6 up-left arrow +2196 ; unqualified # ↖ E0.6 up-left arrow +2195 FE0F ; fully-qualified # ↕️ E0.6 up-down arrow +2195 ; unqualified # ↕ E0.6 up-down arrow +2194 FE0F ; fully-qualified # ↔️ E0.6 left-right arrow +2194 ; unqualified # ↔ E0.6 left-right arrow +21A9 FE0F ; fully-qualified # ↩️ E0.6 right arrow curving left +21A9 ; unqualified # ↩ E0.6 right arrow curving left +21AA FE0F ; fully-qualified # ↪️ E0.6 left arrow curving right +21AA ; unqualified # ↪ E0.6 left arrow curving right +2934 FE0F ; fully-qualified # ⤴️ E0.6 right arrow curving up +2934 ; unqualified # ⤴ E0.6 right arrow curving up +2935 FE0F ; fully-qualified # ⤵️ E0.6 right arrow curving down +2935 ; unqualified # ⤵ E0.6 right arrow curving down +1F503 ; fully-qualified # 🔃 E0.6 clockwise vertical arrows +1F504 ; fully-qualified # 🔄 E1.0 counterclockwise arrows button +1F519 ; fully-qualified # 🔙 E0.6 BACK arrow +1F51A ; fully-qualified # 🔚 E0.6 END arrow +1F51B ; fully-qualified # 🔛 E0.6 ON! arrow +1F51C ; fully-qualified # 🔜 E0.6 SOON arrow +1F51D ; fully-qualified # 🔝 E0.6 TOP arrow + +# subgroup: religion +1F6D0 ; fully-qualified # 🛐 E1.0 place of worship +269B FE0F ; fully-qualified # ⚛️ E1.0 atom symbol +269B ; unqualified # ⚛ E1.0 atom symbol +1F549 FE0F ; fully-qualified # 🕉️ E0.7 om +1F549 ; unqualified # 🕉 E0.7 om +2721 FE0F ; fully-qualified # ✡️ E0.7 star of David +2721 ; unqualified # ✡ E0.7 star of David +2638 FE0F ; fully-qualified # ☸️ E0.7 wheel of dharma +2638 ; unqualified # ☸ E0.7 wheel of dharma +262F FE0F ; fully-qualified # ☯️ E0.7 yin yang +262F ; unqualified # ☯ E0.7 yin yang +271D FE0F ; fully-qualified # ✝️ E0.7 latin cross +271D ; unqualified # ✝ E0.7 latin cross +2626 FE0F ; fully-qualified # ☦️ E1.0 orthodox cross +2626 ; unqualified # ☦ E1.0 orthodox cross +262A FE0F ; fully-qualified # ☪️ E0.7 star and crescent +262A ; unqualified # ☪ E0.7 star and crescent +262E FE0F ; fully-qualified # ☮️ E1.0 peace symbol +262E ; unqualified # ☮ E1.0 peace symbol +1F54E ; fully-qualified # 🕎 E1.0 menorah +1F52F ; fully-qualified # 🔯 E0.6 dotted six-pointed star + +# subgroup: zodiac +2648 ; fully-qualified # ♈ E0.6 Aries +2649 ; fully-qualified # ♉ E0.6 Taurus +264A ; fully-qualified # ♊ E0.6 Gemini +264B ; fully-qualified # ♋ E0.6 Cancer +264C ; fully-qualified # ♌ E0.6 Leo +264D ; fully-qualified # ♍ E0.6 Virgo +264E ; fully-qualified # ♎ E0.6 Libra +264F ; fully-qualified # ♏ E0.6 Scorpio +2650 ; fully-qualified # ♐ E0.6 Sagittarius +2651 ; fully-qualified # ♑ E0.6 Capricorn +2652 ; fully-qualified # ♒ E0.6 Aquarius +2653 ; fully-qualified # ♓ E0.6 Pisces +26CE ; fully-qualified # ⛎ E0.6 Ophiuchus + +# subgroup: av-symbol +1F500 ; fully-qualified # 🔀 E1.0 shuffle tracks button +1F501 ; fully-qualified # 🔁 E1.0 repeat button +1F502 ; fully-qualified # 🔂 E1.0 repeat single button +25B6 FE0F ; fully-qualified # ▶️ E0.6 play button +25B6 ; unqualified # ▶ E0.6 play button +23E9 ; fully-qualified # ⏩ E0.6 fast-forward button +23ED FE0F ; fully-qualified # ⏭️ E0.7 next track button +23ED ; unqualified # ⏭ E0.7 next track button +23EF FE0F ; fully-qualified # ⏯️ E1.0 play or pause button +23EF ; unqualified # ⏯ E1.0 play or pause button +25C0 FE0F ; fully-qualified # ◀️ E0.6 reverse button +25C0 ; unqualified # ◀ E0.6 reverse button +23EA ; fully-qualified # ⏪ E0.6 fast reverse button +23EE FE0F ; fully-qualified # ⏮️ E0.7 last track button +23EE ; unqualified # ⏮ E0.7 last track button +1F53C ; fully-qualified # 🔼 E0.6 upwards button +23EB ; fully-qualified # ⏫ E0.6 fast up button +1F53D ; fully-qualified # 🔽 E0.6 downwards button +23EC ; fully-qualified # ⏬ E0.6 fast down button +23F8 FE0F ; fully-qualified # ⏸️ E0.7 pause button +23F8 ; unqualified # ⏸ E0.7 pause button +23F9 FE0F ; fully-qualified # ⏹️ E0.7 stop button +23F9 ; unqualified # ⏹ E0.7 stop button +23FA FE0F ; fully-qualified # ⏺️ E0.7 record button +23FA ; unqualified # ⏺ E0.7 record button +23CF FE0F ; fully-qualified # ⏏️ E1.0 eject button +23CF ; unqualified # ⏏ E1.0 eject button +1F3A6 ; fully-qualified # 🎦 E0.6 cinema +1F505 ; fully-qualified # 🔅 E1.0 dim button +1F506 ; fully-qualified # 🔆 E1.0 bright button +1F4F6 ; fully-qualified # 📶 E0.6 antenna bars +1F4F3 ; fully-qualified # 📳 E0.6 vibration mode +1F4F4 ; fully-qualified # 📴 E0.6 mobile phone off + +# subgroup: gender +2640 FE0F ; fully-qualified # ♀️ E4.0 female sign +2640 ; unqualified # ♀ E4.0 female sign +2642 FE0F ; fully-qualified # ♂️ E4.0 male sign +2642 ; unqualified # ♂ E4.0 male sign +26A7 FE0F ; fully-qualified # ⚧️ E13.0 transgender symbol +26A7 ; unqualified # ⚧ E13.0 transgender symbol + +# subgroup: other-symbol +2695 FE0F ; fully-qualified # ⚕️ E4.0 medical symbol +2695 ; unqualified # ⚕ E4.0 medical symbol +267E FE0F ; fully-qualified # ♾️ E11.0 infinity +267E ; unqualified # ♾ E11.0 infinity +267B FE0F ; fully-qualified # ♻️ E0.6 recycling symbol +267B ; unqualified # ♻ E0.6 recycling symbol +269C FE0F ; fully-qualified # ⚜️ E1.0 fleur-de-lis +269C ; unqualified # ⚜ E1.0 fleur-de-lis +1F531 ; fully-qualified # 🔱 E0.6 trident emblem +1F4DB ; fully-qualified # 📛 E0.6 name badge +1F530 ; fully-qualified # 🔰 E0.6 Japanese symbol for beginner +2B55 ; fully-qualified # ⭕ E0.6 hollow red circle +2705 ; fully-qualified # ✅ E0.6 check mark button +2611 FE0F ; fully-qualified # ☑️ E0.6 check box with check +2611 ; unqualified # ☑ E0.6 check box with check +2714 FE0F ; fully-qualified # ✔️ E0.6 check mark +2714 ; unqualified # ✔ E0.6 check mark +2716 FE0F ; fully-qualified # ✖️ E0.6 multiplication sign +2716 ; unqualified # ✖ E0.6 multiplication sign +274C ; fully-qualified # ❌ E0.6 cross mark +274E ; fully-qualified # ❎ E0.6 cross mark button +2795 ; fully-qualified # ➕ E0.6 plus sign +2796 ; fully-qualified # ➖ E0.6 minus sign +2797 ; fully-qualified # ➗ E0.6 division sign +27B0 ; fully-qualified # ➰ E0.6 curly loop +27BF ; fully-qualified # ➿ E1.0 double curly loop +303D FE0F ; fully-qualified # 〽️ E0.6 part alternation mark +303D ; unqualified # 〽 E0.6 part alternation mark +2733 FE0F ; fully-qualified # ✳️ E0.6 eight-spoked asterisk +2733 ; unqualified # ✳ E0.6 eight-spoked asterisk +2734 FE0F ; fully-qualified # ✴️ E0.6 eight-pointed star +2734 ; unqualified # ✴ E0.6 eight-pointed star +2747 FE0F ; fully-qualified # ❇️ E0.6 sparkle +2747 ; unqualified # ❇ E0.6 sparkle +203C FE0F ; fully-qualified # ‼️ E0.6 double exclamation mark +203C ; unqualified # ‼ E0.6 double exclamation mark +2049 FE0F ; fully-qualified # ⁉️ E0.6 exclamation question mark +2049 ; unqualified # ⁉ E0.6 exclamation question mark +2753 ; fully-qualified # ❓ E0.6 question mark +2754 ; fully-qualified # ❔ E0.6 white question mark +2755 ; fully-qualified # ❕ E0.6 white exclamation mark +2757 ; fully-qualified # ❗ E0.6 exclamation mark +3030 FE0F ; fully-qualified # 〰️ E0.6 wavy dash +3030 ; unqualified # 〰 E0.6 wavy dash +00A9 FE0F ; fully-qualified # ©️ E0.6 copyright +00A9 ; unqualified # © E0.6 copyright +00AE FE0F ; fully-qualified # ®️ E0.6 registered +00AE ; unqualified # ® E0.6 registered +2122 FE0F ; fully-qualified # ™️ E0.6 trade mark +2122 ; unqualified # ™ E0.6 trade mark + +# subgroup: keycap +0023 FE0F 20E3 ; fully-qualified # #️⃣ E0.6 keycap: # +0023 20E3 ; unqualified # #⃣ E0.6 keycap: # +002A FE0F 20E3 ; fully-qualified # *️⃣ E2.0 keycap: * +002A 20E3 ; unqualified # *⃣ E2.0 keycap: * +0030 FE0F 20E3 ; fully-qualified # 0️⃣ E0.6 keycap: 0 +0030 20E3 ; unqualified # 0⃣ E0.6 keycap: 0 +0031 FE0F 20E3 ; fully-qualified # 1️⃣ E0.6 keycap: 1 +0031 20E3 ; unqualified # 1⃣ E0.6 keycap: 1 +0032 FE0F 20E3 ; fully-qualified # 2️⃣ E0.6 keycap: 2 +0032 20E3 ; unqualified # 2⃣ E0.6 keycap: 2 +0033 FE0F 20E3 ; fully-qualified # 3️⃣ E0.6 keycap: 3 +0033 20E3 ; unqualified # 3⃣ E0.6 keycap: 3 +0034 FE0F 20E3 ; fully-qualified # 4️⃣ E0.6 keycap: 4 +0034 20E3 ; unqualified # 4⃣ E0.6 keycap: 4 +0035 FE0F 20E3 ; fully-qualified # 5️⃣ E0.6 keycap: 5 +0035 20E3 ; unqualified # 5⃣ E0.6 keycap: 5 +0036 FE0F 20E3 ; fully-qualified # 6️⃣ E0.6 keycap: 6 +0036 20E3 ; unqualified # 6⃣ E0.6 keycap: 6 +0037 FE0F 20E3 ; fully-qualified # 7️⃣ E0.6 keycap: 7 +0037 20E3 ; unqualified # 7⃣ E0.6 keycap: 7 +0038 FE0F 20E3 ; fully-qualified # 8️⃣ E0.6 keycap: 8 +0038 20E3 ; unqualified # 8⃣ E0.6 keycap: 8 +0039 FE0F 20E3 ; fully-qualified # 9️⃣ E0.6 keycap: 9 +0039 20E3 ; unqualified # 9⃣ E0.6 keycap: 9 +1F51F ; fully-qualified # 🔟 E0.6 keycap: 10 + +# subgroup: alphanum +1F520 ; fully-qualified # 🔠 E0.6 input latin uppercase +1F521 ; fully-qualified # 🔡 E0.6 input latin lowercase +1F522 ; fully-qualified # 🔢 E0.6 input numbers +1F523 ; fully-qualified # 🔣 E0.6 input symbols +1F524 ; fully-qualified # 🔤 E0.6 input latin letters +1F170 FE0F ; fully-qualified # 🅰️ E0.6 A button (blood type) +1F170 ; unqualified # 🅰 E0.6 A button (blood type) +1F18E ; fully-qualified # 🆎 E0.6 AB button (blood type) +1F171 FE0F ; fully-qualified # 🅱️ E0.6 B button (blood type) +1F171 ; unqualified # 🅱 E0.6 B button (blood type) +1F191 ; fully-qualified # 🆑 E0.6 CL button +1F192 ; fully-qualified # 🆒 E0.6 COOL button +1F193 ; fully-qualified # 🆓 E0.6 FREE button +2139 FE0F ; fully-qualified # ℹ️ E0.6 information +2139 ; unqualified # ℹ E0.6 information +1F194 ; fully-qualified # 🆔 E0.6 ID button +24C2 FE0F ; fully-qualified # Ⓜ️ E0.6 circled M +24C2 ; unqualified # Ⓜ E0.6 circled M +1F195 ; fully-qualified # 🆕 E0.6 NEW button +1F196 ; fully-qualified # 🆖 E0.6 NG button +1F17E FE0F ; fully-qualified # 🅾️ E0.6 O button (blood type) +1F17E ; unqualified # 🅾 E0.6 O button (blood type) +1F197 ; fully-qualified # 🆗 E0.6 OK button +1F17F FE0F ; fully-qualified # 🅿️ E0.6 P button +1F17F ; unqualified # 🅿 E0.6 P button +1F198 ; fully-qualified # 🆘 E0.6 SOS button +1F199 ; fully-qualified # 🆙 E0.6 UP! button +1F19A ; fully-qualified # 🆚 E0.6 VS button +1F201 ; fully-qualified # 🈁 E0.6 Japanese “here” button +1F202 FE0F ; fully-qualified # 🈂️ E0.6 Japanese “service charge” button +1F202 ; unqualified # 🈂 E0.6 Japanese “service charge” button +1F237 FE0F ; fully-qualified # 🈷️ E0.6 Japanese “monthly amount” button +1F237 ; unqualified # 🈷 E0.6 Japanese “monthly amount” button +1F236 ; fully-qualified # 🈶 E0.6 Japanese “not free of charge” button +1F22F ; fully-qualified # 🈯 E0.6 Japanese “reserved” button +1F250 ; fully-qualified # 🉐 E0.6 Japanese “bargain” button +1F239 ; fully-qualified # 🈹 E0.6 Japanese “discount” button +1F21A ; fully-qualified # 🈚 E0.6 Japanese “free of charge” button +1F232 ; fully-qualified # 🈲 E0.6 Japanese “prohibited” button +1F251 ; fully-qualified # 🉑 E0.6 Japanese “acceptable” button +1F238 ; fully-qualified # 🈸 E0.6 Japanese “application” button +1F234 ; fully-qualified # 🈴 E0.6 Japanese “passing grade” button +1F233 ; fully-qualified # 🈳 E0.6 Japanese “vacancy” button +3297 FE0F ; fully-qualified # ㊗️ E0.6 Japanese “congratulations” button +3297 ; unqualified # ㊗ E0.6 Japanese “congratulations” button +3299 FE0F ; fully-qualified # ㊙️ E0.6 Japanese “secret” button +3299 ; unqualified # ㊙ E0.6 Japanese “secret” button +1F23A ; fully-qualified # 🈺 E0.6 Japanese “open for business” button +1F235 ; fully-qualified # 🈵 E0.6 Japanese “no vacancy” button + +# subgroup: geometric +1F534 ; fully-qualified # 🔴 E0.6 red circle +1F7E0 ; fully-qualified # 🟠 E12.0 orange circle +1F7E1 ; fully-qualified # 🟡 E12.0 yellow circle +1F7E2 ; fully-qualified # 🟢 E12.0 green circle +1F535 ; fully-qualified # 🔵 E0.6 blue circle +1F7E3 ; fully-qualified # 🟣 E12.0 purple circle +1F7E4 ; fully-qualified # 🟤 E12.0 brown circle +26AB ; fully-qualified # ⚫ E0.6 black circle +26AA ; fully-qualified # ⚪ E0.6 white circle +1F7E5 ; fully-qualified # 🟥 E12.0 red square +1F7E7 ; fully-qualified # 🟧 E12.0 orange square +1F7E8 ; fully-qualified # 🟨 E12.0 yellow square +1F7E9 ; fully-qualified # 🟩 E12.0 green square +1F7E6 ; fully-qualified # 🟦 E12.0 blue square +1F7EA ; fully-qualified # 🟪 E12.0 purple square +1F7EB ; fully-qualified # 🟫 E12.0 brown square +2B1B ; fully-qualified # ⬛ E0.6 black large square +2B1C ; fully-qualified # ⬜ E0.6 white large square +25FC FE0F ; fully-qualified # ◼️ E0.6 black medium square +25FC ; unqualified # ◼ E0.6 black medium square +25FB FE0F ; fully-qualified # ◻️ E0.6 white medium square +25FB ; unqualified # ◻ E0.6 white medium square +25FE ; fully-qualified # ◾ E0.6 black medium-small square +25FD ; fully-qualified # ◽ E0.6 white medium-small square +25AA FE0F ; fully-qualified # ▪️ E0.6 black small square +25AA ; unqualified # ▪ E0.6 black small square +25AB FE0F ; fully-qualified # ▫️ E0.6 white small square +25AB ; unqualified # ▫ E0.6 white small square +1F536 ; fully-qualified # 🔶 E0.6 large orange diamond +1F537 ; fully-qualified # 🔷 E0.6 large blue diamond +1F538 ; fully-qualified # 🔸 E0.6 small orange diamond +1F539 ; fully-qualified # 🔹 E0.6 small blue diamond +1F53A ; fully-qualified # 🔺 E0.6 red triangle pointed up +1F53B ; fully-qualified # 🔻 E0.6 red triangle pointed down +1F4A0 ; fully-qualified # 💠 E0.6 diamond with a dot +1F518 ; fully-qualified # 🔘 E0.6 radio button +1F533 ; fully-qualified # 🔳 E0.6 white square button +1F532 ; fully-qualified # 🔲 E0.6 black square button + +# Symbols subtotal: 299 +# Symbols subtotal: 299 w/o modifiers + +# group: Flags + +# subgroup: flag +1F3C1 ; fully-qualified # 🏁 E0.6 chequered flag +1F6A9 ; fully-qualified # 🚩 E0.6 triangular flag +1F38C ; fully-qualified # 🎌 E0.6 crossed flags +1F3F4 ; fully-qualified # 🏴 E1.0 black flag +1F3F3 FE0F ; fully-qualified # 🏳️ E0.7 white flag +1F3F3 ; unqualified # 🏳 E0.7 white flag +1F3F3 FE0F 200D 1F308 ; fully-qualified # 🏳️🌈 E4.0 rainbow flag +1F3F3 200D 1F308 ; unqualified # 🏳🌈 E4.0 rainbow flag +1F3F3 FE0F 200D 26A7 FE0F ; fully-qualified # 🏳️⚧️ E13.0 transgender flag +1F3F3 200D 26A7 FE0F ; unqualified # 🏳⚧️ E13.0 transgender flag +1F3F3 FE0F 200D 26A7 ; unqualified # 🏳️⚧ E13.0 transgender flag +1F3F3 200D 26A7 ; unqualified # 🏳⚧ E13.0 transgender flag +1F3F4 200D 2620 FE0F ; fully-qualified # 🏴☠️ E11.0 pirate flag +1F3F4 200D 2620 ; minimally-qualified # 🏴☠ E11.0 pirate flag + +# subgroup: country-flag +1F1E6 1F1E8 ; fully-qualified # 🇦🇨 E2.0 flag: Ascension Island +1F1E6 1F1E9 ; fully-qualified # 🇦🇩 E2.0 flag: Andorra +1F1E6 1F1EA ; fully-qualified # 🇦🇪 E2.0 flag: United Arab Emirates +1F1E6 1F1EB ; fully-qualified # 🇦🇫 E2.0 flag: Afghanistan +1F1E6 1F1EC ; fully-qualified # 🇦🇬 E2.0 flag: Antigua & Barbuda +1F1E6 1F1EE ; fully-qualified # 🇦🇮 E2.0 flag: Anguilla +1F1E6 1F1F1 ; fully-qualified # 🇦🇱 E2.0 flag: Albania +1F1E6 1F1F2 ; fully-qualified # 🇦🇲 E2.0 flag: Armenia +1F1E6 1F1F4 ; fully-qualified # 🇦🇴 E2.0 flag: Angola +1F1E6 1F1F6 ; fully-qualified # 🇦🇶 E2.0 flag: Antarctica +1F1E6 1F1F7 ; fully-qualified # 🇦🇷 E2.0 flag: Argentina +1F1E6 1F1F8 ; fully-qualified # 🇦🇸 E2.0 flag: American Samoa +1F1E6 1F1F9 ; fully-qualified # 🇦🇹 E2.0 flag: Austria +1F1E6 1F1FA ; fully-qualified # 🇦🇺 E2.0 flag: Australia +1F1E6 1F1FC ; fully-qualified # 🇦🇼 E2.0 flag: Aruba +1F1E6 1F1FD ; fully-qualified # 🇦🇽 E2.0 flag: Åland Islands +1F1E6 1F1FF ; fully-qualified # 🇦🇿 E2.0 flag: Azerbaijan +1F1E7 1F1E6 ; fully-qualified # 🇧🇦 E2.0 flag: Bosnia & Herzegovina +1F1E7 1F1E7 ; fully-qualified # 🇧🇧 E2.0 flag: Barbados +1F1E7 1F1E9 ; fully-qualified # 🇧🇩 E2.0 flag: Bangladesh +1F1E7 1F1EA ; fully-qualified # 🇧🇪 E2.0 flag: Belgium +1F1E7 1F1EB ; fully-qualified # 🇧🇫 E2.0 flag: Burkina Faso +1F1E7 1F1EC ; fully-qualified # 🇧🇬 E2.0 flag: Bulgaria +1F1E7 1F1ED ; fully-qualified # 🇧🇭 E2.0 flag: Bahrain +1F1E7 1F1EE ; fully-qualified # 🇧🇮 E2.0 flag: Burundi +1F1E7 1F1EF ; fully-qualified # 🇧🇯 E2.0 flag: Benin +1F1E7 1F1F1 ; fully-qualified # 🇧🇱 E2.0 flag: St. Barthélemy +1F1E7 1F1F2 ; fully-qualified # 🇧🇲 E2.0 flag: Bermuda +1F1E7 1F1F3 ; fully-qualified # 🇧🇳 E2.0 flag: Brunei +1F1E7 1F1F4 ; fully-qualified # 🇧🇴 E2.0 flag: Bolivia +1F1E7 1F1F6 ; fully-qualified # 🇧🇶 E2.0 flag: Caribbean Netherlands +1F1E7 1F1F7 ; fully-qualified # 🇧🇷 E2.0 flag: Brazil +1F1E7 1F1F8 ; fully-qualified # 🇧🇸 E2.0 flag: Bahamas +1F1E7 1F1F9 ; fully-qualified # 🇧🇹 E2.0 flag: Bhutan +1F1E7 1F1FB ; fully-qualified # 🇧🇻 E2.0 flag: Bouvet Island +1F1E7 1F1FC ; fully-qualified # 🇧🇼 E2.0 flag: Botswana +1F1E7 1F1FE ; fully-qualified # 🇧🇾 E2.0 flag: Belarus +1F1E7 1F1FF ; fully-qualified # 🇧🇿 E2.0 flag: Belize +1F1E8 1F1E6 ; fully-qualified # 🇨🇦 E2.0 flag: Canada +1F1E8 1F1E8 ; fully-qualified # 🇨🇨 E2.0 flag: Cocos (Keeling) Islands +1F1E8 1F1E9 ; fully-qualified # 🇨🇩 E2.0 flag: Congo - Kinshasa +1F1E8 1F1EB ; fully-qualified # 🇨🇫 E2.0 flag: Central African Republic +1F1E8 1F1EC ; fully-qualified # 🇨🇬 E2.0 flag: Congo - Brazzaville +1F1E8 1F1ED ; fully-qualified # 🇨🇭 E2.0 flag: Switzerland +1F1E8 1F1EE ; fully-qualified # 🇨🇮 E2.0 flag: Côte d’Ivoire +1F1E8 1F1F0 ; fully-qualified # 🇨🇰 E2.0 flag: Cook Islands +1F1E8 1F1F1 ; fully-qualified # 🇨🇱 E2.0 flag: Chile +1F1E8 1F1F2 ; fully-qualified # 🇨🇲 E2.0 flag: Cameroon +1F1E8 1F1F3 ; fully-qualified # 🇨🇳 E0.6 flag: China +1F1E8 1F1F4 ; fully-qualified # 🇨🇴 E2.0 flag: Colombia +1F1E8 1F1F5 ; fully-qualified # 🇨🇵 E2.0 flag: Clipperton Island +1F1E8 1F1F7 ; fully-qualified # 🇨🇷 E2.0 flag: Costa Rica +1F1E8 1F1FA ; fully-qualified # 🇨🇺 E2.0 flag: Cuba +1F1E8 1F1FB ; fully-qualified # 🇨🇻 E2.0 flag: Cape Verde +1F1E8 1F1FC ; fully-qualified # 🇨🇼 E2.0 flag: Curaçao +1F1E8 1F1FD ; fully-qualified # 🇨🇽 E2.0 flag: Christmas Island +1F1E8 1F1FE ; fully-qualified # 🇨🇾 E2.0 flag: Cyprus +1F1E8 1F1FF ; fully-qualified # 🇨🇿 E2.0 flag: Czechia +1F1E9 1F1EA ; fully-qualified # 🇩🇪 E0.6 flag: Germany +1F1E9 1F1EC ; fully-qualified # 🇩🇬 E2.0 flag: Diego Garcia +1F1E9 1F1EF ; fully-qualified # 🇩🇯 E2.0 flag: Djibouti +1F1E9 1F1F0 ; fully-qualified # 🇩🇰 E2.0 flag: Denmark +1F1E9 1F1F2 ; fully-qualified # 🇩🇲 E2.0 flag: Dominica +1F1E9 1F1F4 ; fully-qualified # 🇩🇴 E2.0 flag: Dominican Republic +1F1E9 1F1FF ; fully-qualified # 🇩🇿 E2.0 flag: Algeria +1F1EA 1F1E6 ; fully-qualified # 🇪🇦 E2.0 flag: Ceuta & Melilla +1F1EA 1F1E8 ; fully-qualified # 🇪🇨 E2.0 flag: Ecuador +1F1EA 1F1EA ; fully-qualified # 🇪🇪 E2.0 flag: Estonia +1F1EA 1F1EC ; fully-qualified # 🇪🇬 E2.0 flag: Egypt +1F1EA 1F1ED ; fully-qualified # 🇪🇭 E2.0 flag: Western Sahara +1F1EA 1F1F7 ; fully-qualified # 🇪🇷 E2.0 flag: Eritrea +1F1EA 1F1F8 ; fully-qualified # 🇪🇸 E0.6 flag: Spain +1F1EA 1F1F9 ; fully-qualified # 🇪🇹 E2.0 flag: Ethiopia +1F1EA 1F1FA ; fully-qualified # 🇪🇺 E2.0 flag: European Union +1F1EB 1F1EE ; fully-qualified # 🇫🇮 E2.0 flag: Finland +1F1EB 1F1EF ; fully-qualified # 🇫🇯 E2.0 flag: Fiji +1F1EB 1F1F0 ; fully-qualified # 🇫🇰 E2.0 flag: Falkland Islands +1F1EB 1F1F2 ; fully-qualified # 🇫🇲 E2.0 flag: Micronesia +1F1EB 1F1F4 ; fully-qualified # 🇫🇴 E2.0 flag: Faroe Islands +1F1EB 1F1F7 ; fully-qualified # 🇫🇷 E0.6 flag: France +1F1EC 1F1E6 ; fully-qualified # 🇬🇦 E2.0 flag: Gabon +1F1EC 1F1E7 ; fully-qualified # 🇬🇧 E0.6 flag: United Kingdom +1F1EC 1F1E9 ; fully-qualified # 🇬🇩 E2.0 flag: Grenada +1F1EC 1F1EA ; fully-qualified # 🇬🇪 E2.0 flag: Georgia +1F1EC 1F1EB ; fully-qualified # 🇬🇫 E2.0 flag: French Guiana +1F1EC 1F1EC ; fully-qualified # 🇬🇬 E2.0 flag: Guernsey +1F1EC 1F1ED ; fully-qualified # 🇬🇭 E2.0 flag: Ghana +1F1EC 1F1EE ; fully-qualified # 🇬🇮 E2.0 flag: Gibraltar +1F1EC 1F1F1 ; fully-qualified # 🇬🇱 E2.0 flag: Greenland +1F1EC 1F1F2 ; fully-qualified # 🇬🇲 E2.0 flag: Gambia +1F1EC 1F1F3 ; fully-qualified # 🇬🇳 E2.0 flag: Guinea +1F1EC 1F1F5 ; fully-qualified # 🇬🇵 E2.0 flag: Guadeloupe +1F1EC 1F1F6 ; fully-qualified # 🇬🇶 E2.0 flag: Equatorial Guinea +1F1EC 1F1F7 ; fully-qualified # 🇬🇷 E2.0 flag: Greece +1F1EC 1F1F8 ; fully-qualified # 🇬🇸 E2.0 flag: South Georgia & South Sandwich Islands +1F1EC 1F1F9 ; fully-qualified # 🇬🇹 E2.0 flag: Guatemala +1F1EC 1F1FA ; fully-qualified # 🇬🇺 E2.0 flag: Guam +1F1EC 1F1FC ; fully-qualified # 🇬🇼 E2.0 flag: Guinea-Bissau +1F1EC 1F1FE ; fully-qualified # 🇬🇾 E2.0 flag: Guyana +1F1ED 1F1F0 ; fully-qualified # 🇭🇰 E2.0 flag: Hong Kong SAR China +1F1ED 1F1F2 ; fully-qualified # 🇭🇲 E2.0 flag: Heard & McDonald Islands +1F1ED 1F1F3 ; fully-qualified # 🇭🇳 E2.0 flag: Honduras +1F1ED 1F1F7 ; fully-qualified # 🇭🇷 E2.0 flag: Croatia +1F1ED 1F1F9 ; fully-qualified # 🇭🇹 E2.0 flag: Haiti +1F1ED 1F1FA ; fully-qualified # 🇭🇺 E2.0 flag: Hungary +1F1EE 1F1E8 ; fully-qualified # 🇮🇨 E2.0 flag: Canary Islands +1F1EE 1F1E9 ; fully-qualified # 🇮🇩 E2.0 flag: Indonesia +1F1EE 1F1EA ; fully-qualified # 🇮🇪 E2.0 flag: Ireland +1F1EE 1F1F1 ; fully-qualified # 🇮🇱 E2.0 flag: Israel +1F1EE 1F1F2 ; fully-qualified # 🇮🇲 E2.0 flag: Isle of Man +1F1EE 1F1F3 ; fully-qualified # 🇮🇳 E2.0 flag: India +1F1EE 1F1F4 ; fully-qualified # 🇮🇴 E2.0 flag: British Indian Ocean Territory +1F1EE 1F1F6 ; fully-qualified # 🇮🇶 E2.0 flag: Iraq +1F1EE 1F1F7 ; fully-qualified # 🇮🇷 E2.0 flag: Iran +1F1EE 1F1F8 ; fully-qualified # 🇮🇸 E2.0 flag: Iceland +1F1EE 1F1F9 ; fully-qualified # 🇮🇹 E0.6 flag: Italy +1F1EF 1F1EA ; fully-qualified # 🇯🇪 E2.0 flag: Jersey +1F1EF 1F1F2 ; fully-qualified # 🇯🇲 E2.0 flag: Jamaica +1F1EF 1F1F4 ; fully-qualified # 🇯🇴 E2.0 flag: Jordan +1F1EF 1F1F5 ; fully-qualified # 🇯🇵 E0.6 flag: Japan +1F1F0 1F1EA ; fully-qualified # 🇰🇪 E2.0 flag: Kenya +1F1F0 1F1EC ; fully-qualified # 🇰🇬 E2.0 flag: Kyrgyzstan +1F1F0 1F1ED ; fully-qualified # 🇰🇭 E2.0 flag: Cambodia +1F1F0 1F1EE ; fully-qualified # 🇰🇮 E2.0 flag: Kiribati +1F1F0 1F1F2 ; fully-qualified # 🇰🇲 E2.0 flag: Comoros +1F1F0 1F1F3 ; fully-qualified # 🇰🇳 E2.0 flag: St. Kitts & Nevis +1F1F0 1F1F5 ; fully-qualified # 🇰🇵 E2.0 flag: North Korea +1F1F0 1F1F7 ; fully-qualified # 🇰🇷 E0.6 flag: South Korea +1F1F0 1F1FC ; fully-qualified # 🇰🇼 E2.0 flag: Kuwait +1F1F0 1F1FE ; fully-qualified # 🇰🇾 E2.0 flag: Cayman Islands +1F1F0 1F1FF ; fully-qualified # 🇰🇿 E2.0 flag: Kazakhstan +1F1F1 1F1E6 ; fully-qualified # 🇱🇦 E2.0 flag: Laos +1F1F1 1F1E7 ; fully-qualified # 🇱🇧 E2.0 flag: Lebanon +1F1F1 1F1E8 ; fully-qualified # 🇱🇨 E2.0 flag: St. Lucia +1F1F1 1F1EE ; fully-qualified # 🇱🇮 E2.0 flag: Liechtenstein +1F1F1 1F1F0 ; fully-qualified # 🇱🇰 E2.0 flag: Sri Lanka +1F1F1 1F1F7 ; fully-qualified # 🇱🇷 E2.0 flag: Liberia +1F1F1 1F1F8 ; fully-qualified # 🇱🇸 E2.0 flag: Lesotho +1F1F1 1F1F9 ; fully-qualified # 🇱🇹 E2.0 flag: Lithuania +1F1F1 1F1FA ; fully-qualified # 🇱🇺 E2.0 flag: Luxembourg +1F1F1 1F1FB ; fully-qualified # 🇱🇻 E2.0 flag: Latvia +1F1F1 1F1FE ; fully-qualified # 🇱🇾 E2.0 flag: Libya +1F1F2 1F1E6 ; fully-qualified # 🇲🇦 E2.0 flag: Morocco +1F1F2 1F1E8 ; fully-qualified # 🇲🇨 E2.0 flag: Monaco +1F1F2 1F1E9 ; fully-qualified # 🇲🇩 E2.0 flag: Moldova +1F1F2 1F1EA ; fully-qualified # 🇲🇪 E2.0 flag: Montenegro +1F1F2 1F1EB ; fully-qualified # 🇲🇫 E2.0 flag: St. Martin +1F1F2 1F1EC ; fully-qualified # 🇲🇬 E2.0 flag: Madagascar +1F1F2 1F1ED ; fully-qualified # 🇲🇭 E2.0 flag: Marshall Islands +1F1F2 1F1F0 ; fully-qualified # 🇲🇰 E2.0 flag: North Macedonia +1F1F2 1F1F1 ; fully-qualified # 🇲🇱 E2.0 flag: Mali +1F1F2 1F1F2 ; fully-qualified # 🇲🇲 E2.0 flag: Myanmar (Burma) +1F1F2 1F1F3 ; fully-qualified # 🇲🇳 E2.0 flag: Mongolia +1F1F2 1F1F4 ; fully-qualified # 🇲🇴 E2.0 flag: Macao SAR China +1F1F2 1F1F5 ; fully-qualified # 🇲🇵 E2.0 flag: Northern Mariana Islands +1F1F2 1F1F6 ; fully-qualified # 🇲🇶 E2.0 flag: Martinique +1F1F2 1F1F7 ; fully-qualified # 🇲🇷 E2.0 flag: Mauritania +1F1F2 1F1F8 ; fully-qualified # 🇲🇸 E2.0 flag: Montserrat +1F1F2 1F1F9 ; fully-qualified # 🇲🇹 E2.0 flag: Malta +1F1F2 1F1FA ; fully-qualified # 🇲🇺 E2.0 flag: Mauritius +1F1F2 1F1FB ; fully-qualified # 🇲🇻 E2.0 flag: Maldives +1F1F2 1F1FC ; fully-qualified # 🇲🇼 E2.0 flag: Malawi +1F1F2 1F1FD ; fully-qualified # 🇲🇽 E2.0 flag: Mexico +1F1F2 1F1FE ; fully-qualified # 🇲🇾 E2.0 flag: Malaysia +1F1F2 1F1FF ; fully-qualified # 🇲🇿 E2.0 flag: Mozambique +1F1F3 1F1E6 ; fully-qualified # 🇳🇦 E2.0 flag: Namibia +1F1F3 1F1E8 ; fully-qualified # 🇳🇨 E2.0 flag: New Caledonia +1F1F3 1F1EA ; fully-qualified # 🇳🇪 E2.0 flag: Niger +1F1F3 1F1EB ; fully-qualified # 🇳🇫 E2.0 flag: Norfolk Island +1F1F3 1F1EC ; fully-qualified # 🇳🇬 E2.0 flag: Nigeria +1F1F3 1F1EE ; fully-qualified # 🇳🇮 E2.0 flag: Nicaragua +1F1F3 1F1F1 ; fully-qualified # 🇳🇱 E2.0 flag: Netherlands +1F1F3 1F1F4 ; fully-qualified # 🇳🇴 E2.0 flag: Norway +1F1F3 1F1F5 ; fully-qualified # 🇳🇵 E2.0 flag: Nepal +1F1F3 1F1F7 ; fully-qualified # 🇳🇷 E2.0 flag: Nauru +1F1F3 1F1FA ; fully-qualified # 🇳🇺 E2.0 flag: Niue +1F1F3 1F1FF ; fully-qualified # 🇳🇿 E2.0 flag: New Zealand +1F1F4 1F1F2 ; fully-qualified # 🇴🇲 E2.0 flag: Oman +1F1F5 1F1E6 ; fully-qualified # 🇵🇦 E2.0 flag: Panama +1F1F5 1F1EA ; fully-qualified # 🇵🇪 E2.0 flag: Peru +1F1F5 1F1EB ; fully-qualified # 🇵🇫 E2.0 flag: French Polynesia +1F1F5 1F1EC ; fully-qualified # 🇵🇬 E2.0 flag: Papua New Guinea +1F1F5 1F1ED ; fully-qualified # 🇵🇭 E2.0 flag: Philippines +1F1F5 1F1F0 ; fully-qualified # 🇵🇰 E2.0 flag: Pakistan +1F1F5 1F1F1 ; fully-qualified # 🇵🇱 E2.0 flag: Poland +1F1F5 1F1F2 ; fully-qualified # 🇵🇲 E2.0 flag: St. Pierre & Miquelon +1F1F5 1F1F3 ; fully-qualified # 🇵🇳 E2.0 flag: Pitcairn Islands +1F1F5 1F1F7 ; fully-qualified # 🇵🇷 E2.0 flag: Puerto Rico +1F1F5 1F1F8 ; fully-qualified # 🇵🇸 E2.0 flag: Palestinian Territories +1F1F5 1F1F9 ; fully-qualified # 🇵🇹 E2.0 flag: Portugal +1F1F5 1F1FC ; fully-qualified # 🇵🇼 E2.0 flag: Palau +1F1F5 1F1FE ; fully-qualified # 🇵🇾 E2.0 flag: Paraguay +1F1F6 1F1E6 ; fully-qualified # 🇶🇦 E2.0 flag: Qatar +1F1F7 1F1EA ; fully-qualified # 🇷🇪 E2.0 flag: Réunion +1F1F7 1F1F4 ; fully-qualified # 🇷🇴 E2.0 flag: Romania +1F1F7 1F1F8 ; fully-qualified # 🇷🇸 E2.0 flag: Serbia +1F1F7 1F1FA ; fully-qualified # 🇷🇺 E0.6 flag: Russia +1F1F7 1F1FC ; fully-qualified # 🇷🇼 E2.0 flag: Rwanda +1F1F8 1F1E6 ; fully-qualified # 🇸🇦 E2.0 flag: Saudi Arabia +1F1F8 1F1E7 ; fully-qualified # 🇸🇧 E2.0 flag: Solomon Islands +1F1F8 1F1E8 ; fully-qualified # 🇸🇨 E2.0 flag: Seychelles +1F1F8 1F1E9 ; fully-qualified # 🇸🇩 E2.0 flag: Sudan +1F1F8 1F1EA ; fully-qualified # 🇸🇪 E2.0 flag: Sweden +1F1F8 1F1EC ; fully-qualified # 🇸🇬 E2.0 flag: Singapore +1F1F8 1F1ED ; fully-qualified # 🇸🇭 E2.0 flag: St. Helena +1F1F8 1F1EE ; fully-qualified # 🇸🇮 E2.0 flag: Slovenia +1F1F8 1F1EF ; fully-qualified # 🇸🇯 E2.0 flag: Svalbard & Jan Mayen +1F1F8 1F1F0 ; fully-qualified # 🇸🇰 E2.0 flag: Slovakia +1F1F8 1F1F1 ; fully-qualified # 🇸🇱 E2.0 flag: Sierra Leone +1F1F8 1F1F2 ; fully-qualified # 🇸🇲 E2.0 flag: San Marino +1F1F8 1F1F3 ; fully-qualified # 🇸🇳 E2.0 flag: Senegal +1F1F8 1F1F4 ; fully-qualified # 🇸🇴 E2.0 flag: Somalia +1F1F8 1F1F7 ; fully-qualified # 🇸🇷 E2.0 flag: Suriname +1F1F8 1F1F8 ; fully-qualified # 🇸🇸 E2.0 flag: South Sudan +1F1F8 1F1F9 ; fully-qualified # 🇸🇹 E2.0 flag: São Tomé & Príncipe +1F1F8 1F1FB ; fully-qualified # 🇸🇻 E2.0 flag: El Salvador +1F1F8 1F1FD ; fully-qualified # 🇸🇽 E2.0 flag: Sint Maarten +1F1F8 1F1FE ; fully-qualified # 🇸🇾 E2.0 flag: Syria +1F1F8 1F1FF ; fully-qualified # 🇸🇿 E2.0 flag: Eswatini +1F1F9 1F1E6 ; fully-qualified # 🇹🇦 E2.0 flag: Tristan da Cunha +1F1F9 1F1E8 ; fully-qualified # 🇹🇨 E2.0 flag: Turks & Caicos Islands +1F1F9 1F1E9 ; fully-qualified # 🇹🇩 E2.0 flag: Chad +1F1F9 1F1EB ; fully-qualified # 🇹🇫 E2.0 flag: French Southern Territories +1F1F9 1F1EC ; fully-qualified # 🇹🇬 E2.0 flag: Togo +1F1F9 1F1ED ; fully-qualified # 🇹🇭 E2.0 flag: Thailand +1F1F9 1F1EF ; fully-qualified # 🇹🇯 E2.0 flag: Tajikistan +1F1F9 1F1F0 ; fully-qualified # 🇹🇰 E2.0 flag: Tokelau +1F1F9 1F1F1 ; fully-qualified # 🇹🇱 E2.0 flag: Timor-Leste +1F1F9 1F1F2 ; fully-qualified # 🇹🇲 E2.0 flag: Turkmenistan +1F1F9 1F1F3 ; fully-qualified # 🇹🇳 E2.0 flag: Tunisia +1F1F9 1F1F4 ; fully-qualified # 🇹🇴 E2.0 flag: Tonga +1F1F9 1F1F7 ; fully-qualified # 🇹🇷 E2.0 flag: Turkey +1F1F9 1F1F9 ; fully-qualified # 🇹🇹 E2.0 flag: Trinidad & Tobago +1F1F9 1F1FB ; fully-qualified # 🇹🇻 E2.0 flag: Tuvalu +1F1F9 1F1FC ; fully-qualified # 🇹🇼 E2.0 flag: Taiwan +1F1F9 1F1FF ; fully-qualified # 🇹🇿 E2.0 flag: Tanzania +1F1FA 1F1E6 ; fully-qualified # 🇺🇦 E2.0 flag: Ukraine +1F1FA 1F1EC ; fully-qualified # 🇺🇬 E2.0 flag: Uganda +1F1FA 1F1F2 ; fully-qualified # 🇺🇲 E2.0 flag: U.S. Outlying Islands +1F1FA 1F1F3 ; fully-qualified # 🇺🇳 E4.0 flag: United Nations +1F1FA 1F1F8 ; fully-qualified # 🇺🇸 E0.6 flag: United States +1F1FA 1F1FE ; fully-qualified # 🇺🇾 E2.0 flag: Uruguay +1F1FA 1F1FF ; fully-qualified # 🇺🇿 E2.0 flag: Uzbekistan +1F1FB 1F1E6 ; fully-qualified # 🇻🇦 E2.0 flag: Vatican City +1F1FB 1F1E8 ; fully-qualified # 🇻🇨 E2.0 flag: St. Vincent & Grenadines +1F1FB 1F1EA ; fully-qualified # 🇻🇪 E2.0 flag: Venezuela +1F1FB 1F1EC ; fully-qualified # 🇻🇬 E2.0 flag: British Virgin Islands +1F1FB 1F1EE ; fully-qualified # 🇻🇮 E2.0 flag: U.S. Virgin Islands +1F1FB 1F1F3 ; fully-qualified # 🇻🇳 E2.0 flag: Vietnam +1F1FB 1F1FA ; fully-qualified # 🇻🇺 E2.0 flag: Vanuatu +1F1FC 1F1EB ; fully-qualified # 🇼🇫 E2.0 flag: Wallis & Futuna +1F1FC 1F1F8 ; fully-qualified # 🇼🇸 E2.0 flag: Samoa +1F1FD 1F1F0 ; fully-qualified # 🇽🇰 E2.0 flag: Kosovo +1F1FE 1F1EA ; fully-qualified # 🇾🇪 E2.0 flag: Yemen +1F1FE 1F1F9 ; fully-qualified # 🇾🇹 E2.0 flag: Mayotte +1F1FF 1F1E6 ; fully-qualified # 🇿🇦 E2.0 flag: South Africa +1F1FF 1F1F2 ; fully-qualified # 🇿🇲 E2.0 flag: Zambia +1F1FF 1F1FC ; fully-qualified # 🇿🇼 E2.0 flag: Zimbabwe + +# subgroup: subdivision-flag +1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; fully-qualified # 🏴 E5.0 flag: England +1F3F4 E0067 E0062 E0073 E0063 E0074 E007F ; fully-qualified # 🏴 E5.0 flag: Scotland +1F3F4 E0067 E0062 E0077 E006C E0073 E007F ; fully-qualified # 🏴 E5.0 flag: Wales + +# Flags subtotal: 275 +# Flags subtotal: 275 w/o modifiers + +# Status Counts +# fully-qualified : 3290 +# minimally-qualified : 614 +# unqualified : 250 +# component : 9 + +#EOF diff --git a/resources/emoji.json b/resources/emoji.json deleted file mode 100644 index 791fa190..00000000 --- a/resources/emoji.json +++ /dev/null @@ -1 +0,0 @@ -{"grinning":{"unicode":"1f600","unicode_alt":"","code_decimal":"😀","name":"grinning face","shortname":":grinning:","category":"people","emoji_order":"1","aliases":[],"aliases_ascii":[],"keywords":["happy","smiley","emotion"]},"grin":{"unicode":"1f601","unicode_alt":"","code_decimal":"😁","name":"grinning face with smiling eyes","shortname":":grin:","category":"people","emoji_order":"2","aliases":[],"aliases_ascii":[],"keywords":["happy","silly","smiley","emotion","good","selfie"]},"joy":{"unicode":"1f602","unicode_alt":"","code_decimal":"😂","name":"face with tears of joy","shortname":":joy:","category":"people","emoji_order":"3","aliases":[],"aliases_ascii":[":')",":'-)"],"keywords":["happy","silly","smiley","cry","laugh","emotion","sarcastic"]},"rofl":{"unicode":"1f923","unicode_alt":"","code_decimal":"🤣","name":"rolling on the floor laughing","shortname":":rofl:","category":"people","emoji_order":"4","aliases":[":rolling_on_the_floor_laughing:"],"aliases_ascii":[],"keywords":[]},"smiley":{"unicode":"1f603","unicode_alt":"","code_decimal":"😃","name":"smiling face with open mouth","shortname":":smiley:","category":"people","emoji_order":"5","aliases":[],"aliases_ascii":[":D",":-D","=D"],"keywords":["happy","smiley","emotion","good"]},"smile":{"unicode":"1f604","unicode_alt":"","code_decimal":"😄","name":"smiling face with open mouth and smiling eyes","shortname":":smile:","category":"people","emoji_order":"6","aliases":[],"aliases_ascii":[],"keywords":["happy","smiley","emotion"]},"sweat_smile":{"unicode":"1f605","unicode_alt":"","code_decimal":"😅","name":"smiling face with open mouth and cold sweat","shortname":":sweat_smile:","category":"people","emoji_order":"7","aliases":[],"aliases_ascii":["':)","':-)","'=)","':D","':-D","'=D"],"keywords":["smiley","workout","sweat","emotion"]},"laughing":{"unicode":"1f606","unicode_alt":"","code_decimal":"😆","name":"smiling face with open mouth and tightly-closed eyes","shortname":":laughing:","category":"people","emoji_order":"8","aliases":[":satisfied:"],"aliases_ascii":[">:)",">;)",">:-)",">=)"],"keywords":["happy","smiley","laugh","emotion"]},"wink":{"unicode":"1f609","unicode_alt":"","code_decimal":"😉","name":"winking face","shortname":":wink:","category":"people","emoji_order":"9","aliases":[],"aliases_ascii":[";)",";-)","*-)","*)",";-]",";]",";D",";^)"],"keywords":["silly","smiley","emotion"]},"blush":{"unicode":"1f60a","unicode_alt":"","code_decimal":"😊","name":"smiling face with smiling eyes","shortname":":blush:","category":"people","emoji_order":"10","aliases":[],"aliases_ascii":[],"keywords":["happy","smiley","emotion","good","beautiful"]},"yum":{"unicode":"1f60b","unicode_alt":"","code_decimal":"😋","name":"face savouring delicious food","shortname":":yum:","category":"people","emoji_order":"11","aliases":[],"aliases_ascii":[],"keywords":["happy","silly","smiley","emotion","sarcastic","good"]},"sunglasses":{"unicode":"1f60e","unicode_alt":"","code_decimal":"😎","name":"smiling face with sunglasses","shortname":":sunglasses:","category":"people","emoji_order":"12","aliases":[],"aliases_ascii":["B-)","B)","8)","8-)","B-D","8-D"],"keywords":["silly","smiley","emojione","glasses","boys night"]},"heart_eyes":{"unicode":"1f60d","unicode_alt":"","code_decimal":"😍","name":"smiling face with heart-shaped eyes","shortname":":heart_eyes:","category":"people","emoji_order":"13","aliases":[],"aliases_ascii":[],"keywords":["happy","smiley","love","sex","heart eyes","emotion","beautiful"]},"kissing_heart":{"unicode":"1f618","unicode_alt":"","code_decimal":"😘","name":"face throwing a kiss","shortname":":kissing_heart:","category":"people","emoji_order":"14","aliases":[],"aliases_ascii":[":*",":-*","=*",":^*"],"keywords":["smiley","love","sexy"]},"kissing":{"unicode":"1f617","unicode_alt":"","code_decimal":"😗","name":"kissing face","shortname":":kissing:","category":"people","emoji_order":"15","aliases":[],"aliases_ascii":[],"keywords":["smiley","sexy"]},"kissing_smiling_eyes":{"unicode":"1f619","unicode_alt":"","code_decimal":"😙","name":"kissing face with smiling eyes","shortname":":kissing_smiling_eyes:","category":"people","emoji_order":"16","aliases":[],"aliases_ascii":[],"keywords":["smiley","sexy"]},"kissing_closed_eyes":{"unicode":"1f61a","unicode_alt":"","code_decimal":"😚","name":"kissing face with closed eyes","shortname":":kissing_closed_eyes:","category":"people","emoji_order":"17","aliases":[],"aliases_ascii":[],"keywords":["smiley","sexy"]},"relaxed":{"unicode":"263a","unicode_alt":"263a-fe0f","code_decimal":"☺","name":"white smiling face","shortname":":relaxed:","category":"people","emoji_order":"18","aliases":[],"aliases_ascii":[],"keywords":["happy","smiley"]},"slight_smile":{"unicode":"1f642","unicode_alt":"","code_decimal":"🙂","name":"slightly smiling face","shortname":":slight_smile:","category":"people","emoji_order":"19","aliases":[":slightly_smiling_face:"],"aliases_ascii":[":)",":-)","=]","=)",":]"],"keywords":["happy","smiley"]},"hugging":{"unicode":"1f917","unicode_alt":"","code_decimal":"🤗","name":"hugging face","shortname":":hugging:","category":"people","emoji_order":"20","aliases":[":hugging_face:"],"aliases_ascii":[],"keywords":["smiley","hug","thank you"]},"thinking":{"unicode":"1f914","unicode_alt":"","code_decimal":"🤔","name":"thinking face","shortname":":thinking:","category":"people","emoji_order":"21","aliases":[":thinking_face:"],"aliases_ascii":[],"keywords":["smiley","thinking","boys night"]},"neutral_face":{"unicode":"1f610","unicode_alt":"","code_decimal":"😐","name":"neutral face","shortname":":neutral_face:","category":"people","emoji_order":"22","aliases":[],"aliases_ascii":[],"keywords":["mad","smiley","shrug","neutral","emotion"]},"expressionless":{"unicode":"1f611","unicode_alt":"","code_decimal":"😑","name":"expressionless face","shortname":":expressionless:","category":"people","emoji_order":"23","aliases":[],"aliases_ascii":["-_-","-__-","-___-"],"keywords":["mad","smiley","neutral","emotion"]},"no_mouth":{"unicode":"1f636","unicode_alt":"","code_decimal":"😶","name":"face without mouth","shortname":":no_mouth:","category":"people","emoji_order":"24","aliases":[],"aliases_ascii":[":-X",":X",":-#",":#","=X","=x",":x",":-x","=#"],"keywords":["mad","smiley","neutral","emotion"]},"rolling_eyes":{"unicode":"1f644","unicode_alt":"","code_decimal":"🙄","name":"face with rolling eyes","shortname":":rolling_eyes:","category":"people","emoji_order":"25","aliases":[":face_with_rolling_eyes:"],"aliases_ascii":[],"keywords":["mad","smiley","rolling eyes","emotion","sarcastic"]},"smirk":{"unicode":"1f60f","unicode_alt":"","code_decimal":"😏","name":"smirking face","shortname":":smirk:","category":"people","emoji_order":"26","aliases":[],"aliases_ascii":[],"keywords":["silly","smiley","sexy","sarcastic"]},"persevere":{"unicode":"1f623","unicode_alt":"","code_decimal":"😣","name":"persevering face","shortname":":persevere:","category":"people","emoji_order":"27","aliases":[],"aliases_ascii":[">.<"],"keywords":["sad","smiley","angry","emotion"]},"disappointed_relieved":{"unicode":"1f625","unicode_alt":"","code_decimal":"😥","name":"disappointed but relieved face","shortname":":disappointed_relieved:","category":"people","emoji_order":"28","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","stressed","sweat","cry","emotion"]},"open_mouth":{"unicode":"1f62e","unicode_alt":"","code_decimal":"😮","name":"face with open mouth","shortname":":open_mouth:","category":"people","emoji_order":"29","aliases":[],"aliases_ascii":[":-O",":O",":-o",":o","O_O",">:O"],"keywords":["smiley","surprised","wow","emotion"]},"zipper_mouth":{"unicode":"1f910","unicode_alt":"","code_decimal":"🤐","name":"zipper-mouth face","shortname":":zipper_mouth:","category":"people","emoji_order":"30","aliases":[":zipper_mouth_face:"],"aliases_ascii":[],"keywords":["mad","smiley"]},"hushed":{"unicode":"1f62f","unicode_alt":"","code_decimal":"😯","name":"hushed face","shortname":":hushed:","category":"people","emoji_order":"31","aliases":[],"aliases_ascii":[],"keywords":["smiley","surprised","wow"]},"sleepy":{"unicode":"1f62a","unicode_alt":"","code_decimal":"😪","name":"sleepy face","shortname":":sleepy:","category":"people","emoji_order":"32","aliases":[],"aliases_ascii":[],"keywords":["smiley","sick","emotion"]},"tired_face":{"unicode":"1f62b","unicode_alt":"","code_decimal":"😫","name":"tired face","shortname":":tired_face:","category":"people","emoji_order":"33","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","tired","emotion"]},"sleeping":{"unicode":"1f634","unicode_alt":"","code_decimal":"😴","name":"sleeping face","shortname":":sleeping:","category":"people","emoji_order":"34","aliases":[],"aliases_ascii":[],"keywords":["smiley","tired","emotion","goodnight"]},"relieved":{"unicode":"1f60c","unicode_alt":"","code_decimal":"😌","name":"relieved face","shortname":":relieved:","category":"people","emoji_order":"35","aliases":[],"aliases_ascii":[],"keywords":["smiley","emotion"]},"nerd":{"unicode":"1f913","unicode_alt":"","code_decimal":"🤓","name":"nerd face","shortname":":nerd:","category":"people","emoji_order":"36","aliases":[":nerd_face:"],"aliases_ascii":[],"keywords":["smiley","glasses"]},"stuck_out_tongue":{"unicode":"1f61b","unicode_alt":"","code_decimal":"😛","name":"face with stuck-out tongue","shortname":":stuck_out_tongue:","category":"people","emoji_order":"37","aliases":[],"aliases_ascii":[":P",":-P","=P",":-p",":p","=p",":-\u00de",":\u00de",":\u00fe",":-\u00fe",":-b",":b","d:"],"keywords":["smiley","sex","emotion"]},"stuck_out_tongue_winking_eye":{"unicode":"1f61c","unicode_alt":"","code_decimal":"😜","name":"face with stuck-out tongue and winking eye","shortname":":stuck_out_tongue_winking_eye:","category":"people","emoji_order":"38","aliases":[],"aliases_ascii":[">:P","X-P","x-p"],"keywords":["happy","smiley","emotion","parties"]},"stuck_out_tongue_closed_eyes":{"unicode":"1f61d","unicode_alt":"","code_decimal":"😝","name":"face with stuck-out tongue and tightly-closed eyes","shortname":":stuck_out_tongue_closed_eyes:","category":"people","emoji_order":"39","aliases":[],"aliases_ascii":[],"keywords":["happy","smiley","emotion"]},"drooling_face":{"unicode":"1f924","unicode_alt":"","code_decimal":"🤤","name":"drooling face","shortname":":drooling_face:","category":"people","emoji_order":"40","aliases":[":drool:"],"aliases_ascii":[],"keywords":[]},"unamused":{"unicode":"1f612","unicode_alt":"","code_decimal":"😒","name":"unamused face","shortname":":unamused:","category":"people","emoji_order":"41","aliases":[],"aliases_ascii":[],"keywords":["sad","mad","smiley","tired","emotion"]},"sweat":{"unicode":"1f613","unicode_alt":"","code_decimal":"😓","name":"face with cold sweat","shortname":":sweat:","category":"people","emoji_order":"42","aliases":[],"aliases_ascii":["':(","':-(","'=("],"keywords":["sad","smiley","stressed","sweat","emotion"]},"pensive":{"unicode":"1f614","unicode_alt":"","code_decimal":"😔","name":"pensive face","shortname":":pensive:","category":"people","emoji_order":"43","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","emotion","rip"]},"confused":{"unicode":"1f615","unicode_alt":"","code_decimal":"😕","name":"confused face","shortname":":confused:","category":"people","emoji_order":"44","aliases":[],"aliases_ascii":[">:\\",">:\/",":-\/",":-.",":\/",":\\","=\/","=\\",":L","=L"],"keywords":["smiley","surprised","emotion"]},"upside_down":{"unicode":"1f643","unicode_alt":"","code_decimal":"🙃","name":"upside-down face","shortname":":upside_down:","category":"people","emoji_order":"45","aliases":[":upside_down_face:"],"aliases_ascii":[],"keywords":["silly","smiley","sarcastic"]},"money_mouth":{"unicode":"1f911","unicode_alt":"","code_decimal":"🤑","name":"money-mouth face","shortname":":money_mouth:","category":"people","emoji_order":"46","aliases":[":money_mouth_face:"],"aliases_ascii":[],"keywords":["smiley","win","money","emotion","boys night"]},"astonished":{"unicode":"1f632","unicode_alt":"","code_decimal":"😲","name":"astonished face","shortname":":astonished:","category":"people","emoji_order":"47","aliases":[],"aliases_ascii":[],"keywords":["smiley","surprised","wow","emotion","omg"]},"frowning2":{"unicode":"2639","unicode_alt":"2639-fe0f","code_decimal":"☹","name":"white frowning face","shortname":":frowning2:","category":"people","emoji_order":"48","aliases":[":white_frowning_face:"],"aliases_ascii":[],"keywords":["sad","smiley","emotion"]},"slight_frown":{"unicode":"1f641","unicode_alt":"","code_decimal":"🙁","name":"slightly frowning face","shortname":":slight_frown:","category":"people","emoji_order":"49","aliases":[":slightly_frowning_face:"],"aliases_ascii":[],"keywords":["sad","smiley","emotion"]},"confounded":{"unicode":"1f616","unicode_alt":"","code_decimal":"😖","name":"confounded face","shortname":":confounded:","category":"people","emoji_order":"50","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","angry","emotion"]},"disappointed":{"unicode":"1f61e","unicode_alt":"","code_decimal":"😞","name":"disappointed face","shortname":":disappointed:","category":"people","emoji_order":"51","aliases":[],"aliases_ascii":[">:[",":-(",":(",":-[",":[","=("],"keywords":["sad","smiley","tired","emotion"]},"worried":{"unicode":"1f61f","unicode_alt":"","code_decimal":"😟","name":"worried face","shortname":":worried:","category":"people","emoji_order":"52","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","emotion"]},"triumph":{"unicode":"1f624","unicode_alt":"","code_decimal":"😤","name":"face with look of triumph","shortname":":triumph:","category":"people","emoji_order":"53","aliases":[],"aliases_ascii":[],"keywords":["mad","smiley","angry","emotion","steam"]},"cry":{"unicode":"1f622","unicode_alt":"","code_decimal":"😢","name":"crying face","shortname":":cry:","category":"people","emoji_order":"54","aliases":[],"aliases_ascii":[":'(",":'-(",";(",";-("],"keywords":["sad","smiley","cry","emotion","rip","heartbreak"]},"sob":{"unicode":"1f62d","unicode_alt":"","code_decimal":"😭","name":"loudly crying face","shortname":":sob:","category":"people","emoji_order":"55","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","cry","emotion","heartbreak"]},"frowning":{"unicode":"1f626","unicode_alt":"","code_decimal":"😦","name":"frowning face with open mouth","shortname":":frowning:","category":"people","emoji_order":"56","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","surprised","emotion"]},"anguished":{"unicode":"1f627","unicode_alt":"","code_decimal":"😧","name":"anguished face","shortname":":anguished:","category":"people","emoji_order":"57","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","surprised","emotion"]},"fearful":{"unicode":"1f628","unicode_alt":"","code_decimal":"😨","name":"fearful face","shortname":":fearful:","category":"people","emoji_order":"58","aliases":[],"aliases_ascii":["D:"],"keywords":["smiley","surprised","emotion"]},"weary":{"unicode":"1f629","unicode_alt":"","code_decimal":"😩","name":"weary face","shortname":":weary:","category":"people","emoji_order":"59","aliases":[],"aliases_ascii":[],"keywords":["sad","smiley","tired","stressed","emotion"]},"grimacing":{"unicode":"1f62c","unicode_alt":"","code_decimal":"😬","name":"grimacing face","shortname":":grimacing:","category":"people","emoji_order":"60","aliases":[],"aliases_ascii":[],"keywords":["silly","smiley","emotion","selfie"]},"cold_sweat":{"unicode":"1f630","unicode_alt":"","code_decimal":"😰","name":"face with open mouth and cold sweat","shortname":":cold_sweat:","category":"people","emoji_order":"61","aliases":[],"aliases_ascii":[],"keywords":["smiley","sweat","emotion"]},"scream":{"unicode":"1f631","unicode_alt":"","code_decimal":"😱","name":"face screaming in fear","shortname":":scream:","category":"people","emoji_order":"62","aliases":[],"aliases_ascii":[],"keywords":["smiley","surprised","wow","emotion","omg"]},"flushed":{"unicode":"1f633","unicode_alt":"","code_decimal":"😳","name":"flushed face","shortname":":flushed:","category":"people","emoji_order":"63","aliases":[],"aliases_ascii":[":$","=$"],"keywords":["smiley","emotion","omg"]},"dizzy_face":{"unicode":"1f635","unicode_alt":"","code_decimal":"😵","name":"dizzy face","shortname":":dizzy_face:","category":"people","emoji_order":"64","aliases":[],"aliases_ascii":["#-)","#)","%-)","%)","X)","X-)"],"keywords":["smiley","surprised","dead","wow","emotion","omg"]},"rage":{"unicode":"1f621","unicode_alt":"","code_decimal":"😡","name":"pouting face","shortname":":rage:","category":"people","emoji_order":"65","aliases":[],"aliases_ascii":[],"keywords":["mad","smiley","angry","emotion"]},"angry":{"unicode":"1f620","unicode_alt":"","code_decimal":"😠","name":"angry face","shortname":":angry:","category":"people","emoji_order":"66","aliases":[],"aliases_ascii":[">:(",">:-(",":@"],"keywords":["mad","smiley","emotion"]},"innocent":{"unicode":"1f607","unicode_alt":"","code_decimal":"😇","name":"smiling face with halo","shortname":":innocent:","category":"people","emoji_order":"67","aliases":[],"aliases_ascii":["O:-)","0:-3","0:3","0:-)","0:)","0;^)","O:)","O;-)","O=)","0;-)","O:-3","O:3"],"keywords":["smiley","emotion"]},"cowboy":{"unicode":"1f920","unicode_alt":"","code_decimal":"🤠","name":"face with cowboy hat","shortname":":cowboy:","category":"people","emoji_order":"68","aliases":[":face_with_cowboy_hat:"],"aliases_ascii":[],"keywords":[]},"clown":{"unicode":"1f921","unicode_alt":"","code_decimal":"🤡","name":"clown face","shortname":":clown:","category":"people","emoji_order":"69","aliases":[":clown_face:"],"aliases_ascii":[],"keywords":[]},"lying_face":{"unicode":"1f925","unicode_alt":"","code_decimal":"🤥","name":"lying face","shortname":":lying_face:","category":"people","emoji_order":"70","aliases":[":liar:"],"aliases_ascii":[],"keywords":[]},"mask":{"unicode":"1f637","unicode_alt":"","code_decimal":"😷","name":"face with medical mask","shortname":":mask:","category":"people","emoji_order":"71","aliases":[],"aliases_ascii":[],"keywords":["smiley","dead","health","sick"]},"thermometer_face":{"unicode":"1f912","unicode_alt":"","code_decimal":"🤒","name":"face with thermometer","shortname":":thermometer_face:","category":"people","emoji_order":"72","aliases":[":face_with_thermometer:"],"aliases_ascii":[],"keywords":["smiley","health","sick","emotion"]},"head_bandage":{"unicode":"1f915","unicode_alt":"","code_decimal":"🤕","name":"face with head-bandage","shortname":":head_bandage:","category":"people","emoji_order":"73","aliases":[":face_with_head_bandage:"],"aliases_ascii":[],"keywords":["smiley","health","sick","emotion"]},"nauseated_face":{"unicode":"1f922","unicode_alt":"","code_decimal":"🤢","name":"nauseated face","shortname":":nauseated_face:","category":"people","emoji_order":"74","aliases":[":sick:"],"aliases_ascii":[],"keywords":[]},"sneezing_face":{"unicode":"1f927","unicode_alt":"","code_decimal":"🤧","name":"sneezing face","shortname":":sneezing_face:","category":"people","emoji_order":"75","aliases":[":sneeze:"],"aliases_ascii":[],"keywords":[]},"smiling_imp":{"unicode":"1f608","unicode_alt":"","code_decimal":"😈","name":"smiling face with horns","shortname":":smiling_imp:","category":"people","emoji_order":"76","aliases":[],"aliases_ascii":[],"keywords":["silly","smiley","angry","monster","devil","boys night"]},"imp":{"unicode":"1f47f","unicode_alt":"","code_decimal":"👿","name":"imp","shortname":":imp:","category":"people","emoji_order":"77","aliases":[],"aliases_ascii":[],"keywords":["smiley","monster","devil","wth"]},"japanese_ogre":{"unicode":"1f479","unicode_alt":"","code_decimal":"👹","name":"japanese ogre","shortname":":japanese_ogre:","category":"people","emoji_order":"78","aliases":[],"aliases_ascii":[],"keywords":["monster"]},"japanese_goblin":{"unicode":"1f47a","unicode_alt":"","code_decimal":"👺","name":"japanese goblin","shortname":":japanese_goblin:","category":"people","emoji_order":"79","aliases":[],"aliases_ascii":[],"keywords":["angry","monster"]},"skull":{"unicode":"1f480","unicode_alt":"","code_decimal":"💀","name":"skull","shortname":":skull:","category":"people","emoji_order":"80","aliases":[":skeleton:"],"aliases_ascii":[],"keywords":["dead","halloween","skull"]},"skull_crossbones":{"unicode":"2620","unicode_alt":"2620-fe0f","code_decimal":"☠","name":"skull and crossbones","shortname":":skull_crossbones:","category":"objects","emoji_order":"81","aliases":[":skull_and_crossbones:"],"aliases_ascii":[],"keywords":["symbol","dead","skull"]},"ghost":{"unicode":"1f47b","unicode_alt":"","code_decimal":"👻","name":"ghost","shortname":":ghost:","category":"people","emoji_order":"82","aliases":[],"aliases_ascii":[],"keywords":["holidays","halloween","monster"]},"alien":{"unicode":"1f47d","unicode_alt":"","code_decimal":"👽","name":"extraterrestrial alien","shortname":":alien:","category":"people","emoji_order":"83","aliases":[],"aliases_ascii":[],"keywords":["space","monster","alien","scientology"]},"space_invader":{"unicode":"1f47e","unicode_alt":"","code_decimal":"👾","name":"alien monster","shortname":":space_invader:","category":"activity","emoji_order":"84","aliases":[],"aliases_ascii":[],"keywords":["monster","alien"]},"robot":{"unicode":"1f916","unicode_alt":"","code_decimal":"🤖","name":"robot face","shortname":":robot:","category":"people","emoji_order":"85","aliases":[":robot_face:"],"aliases_ascii":[],"keywords":["monster","robot"]},"poop":{"unicode":"1f4a9","unicode_alt":"","code_decimal":"💩","name":"pile of poo","shortname":":poop:","category":"people","emoji_order":"86","aliases":[":shit:",":hankey:",":poo:"],"aliases_ascii":[],"keywords":["bathroom","shit","sol","diarrhea"]},"smiley_cat":{"unicode":"1f63a","unicode_alt":"","code_decimal":"😺","name":"smiling cat face with open mouth","shortname":":smiley_cat:","category":"people","emoji_order":"87","aliases":[],"aliases_ascii":[],"keywords":["happy","cat","animal"]},"smile_cat":{"unicode":"1f638","unicode_alt":"","code_decimal":"😸","name":"grinning cat face with smiling eyes","shortname":":smile_cat:","category":"people","emoji_order":"88","aliases":[],"aliases_ascii":[],"keywords":["happy","cat","animal"]},"joy_cat":{"unicode":"1f639","unicode_alt":"","code_decimal":"😹","name":"cat face with tears of joy","shortname":":joy_cat:","category":"people","emoji_order":"89","aliases":[],"aliases_ascii":[],"keywords":["happy","silly","cry","laugh","cat","animal","sarcastic"]},"heart_eyes_cat":{"unicode":"1f63b","unicode_alt":"","code_decimal":"😻","name":"smiling cat face with heart-shaped eyes","shortname":":heart_eyes_cat:","category":"people","emoji_order":"90","aliases":[],"aliases_ascii":[],"keywords":["heart eyes","cat","animal","beautiful"]},"smirk_cat":{"unicode":"1f63c","unicode_alt":"","code_decimal":"😼","name":"cat face with wry smile","shortname":":smirk_cat:","category":"people","emoji_order":"91","aliases":[],"aliases_ascii":[],"keywords":["cat","animal"]},"kissing_cat":{"unicode":"1f63d","unicode_alt":"","code_decimal":"😽","name":"kissing cat face with closed eyes","shortname":":kissing_cat:","category":"people","emoji_order":"92","aliases":[],"aliases_ascii":[],"keywords":["cat","animal"]},"scream_cat":{"unicode":"1f640","unicode_alt":"","code_decimal":"🙀","name":"weary cat face","shortname":":scream_cat:","category":"people","emoji_order":"93","aliases":[],"aliases_ascii":[],"keywords":["cat","animal"]},"crying_cat_face":{"unicode":"1f63f","unicode_alt":"","code_decimal":"😿","name":"crying cat face","shortname":":crying_cat_face:","category":"people","emoji_order":"94","aliases":[],"aliases_ascii":[],"keywords":["cry","cat","animal"]},"pouting_cat":{"unicode":"1f63e","unicode_alt":"","code_decimal":"😾","name":"pouting cat face","shortname":":pouting_cat:","category":"people","emoji_order":"95","aliases":[],"aliases_ascii":[],"keywords":["cat","animal"]},"see_no_evil":{"unicode":"1f648","unicode_alt":"","code_decimal":"🙈","name":"see-no-evil monkey","shortname":":see_no_evil:","category":"nature","emoji_order":"96","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"hear_no_evil":{"unicode":"1f649","unicode_alt":"","code_decimal":"🙉","name":"hear-no-evil monkey","shortname":":hear_no_evil:","category":"nature","emoji_order":"97","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"speak_no_evil":{"unicode":"1f64a","unicode_alt":"","code_decimal":"🙊","name":"speak-no-evil monkey","shortname":":speak_no_evil:","category":"nature","emoji_order":"98","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"boy":{"unicode":"1f466","unicode_alt":"","code_decimal":"👦","name":"boy","shortname":":boy:","category":"people","emoji_order":"99","aliases":[],"aliases_ascii":[],"keywords":["people","baby","diversity"]},"boy_tone1":{"unicode":"1f466-1f3fb","unicode_alt":"","code_decimal":"👦🏻","name":"boy tone 1","shortname":":boy_tone1:","category":"people","emoji_order":"100","aliases":[],"aliases_ascii":[],"keywords":[]},"boy_tone2":{"unicode":"1f466-1f3fc","unicode_alt":"","code_decimal":"👦🏼","name":"boy tone 2","shortname":":boy_tone2:","category":"people","emoji_order":"101","aliases":[],"aliases_ascii":[],"keywords":[]},"boy_tone3":{"unicode":"1f466-1f3fd","unicode_alt":"","code_decimal":"👦🏽","name":"boy tone 3","shortname":":boy_tone3:","category":"people","emoji_order":"102","aliases":[],"aliases_ascii":[],"keywords":[]},"boy_tone4":{"unicode":"1f466-1f3fe","unicode_alt":"","code_decimal":"👦🏾","name":"boy tone 4","shortname":":boy_tone4:","category":"people","emoji_order":"103","aliases":[],"aliases_ascii":[],"keywords":[]},"boy_tone5":{"unicode":"1f466-1f3ff","unicode_alt":"","code_decimal":"👦🏿","name":"boy tone 5","shortname":":boy_tone5:","category":"people","emoji_order":"104","aliases":[],"aliases_ascii":[],"keywords":[]},"girl":{"unicode":"1f467","unicode_alt":"","code_decimal":"👧","name":"girl","shortname":":girl:","category":"people","emoji_order":"105","aliases":[],"aliases_ascii":[],"keywords":["people","women","baby","diversity"]},"girl_tone1":{"unicode":"1f467-1f3fb","unicode_alt":"","code_decimal":"👧🏻","name":"girl tone 1","shortname":":girl_tone1:","category":"people","emoji_order":"106","aliases":[],"aliases_ascii":[],"keywords":[]},"girl_tone2":{"unicode":"1f467-1f3fc","unicode_alt":"","code_decimal":"👧🏼","name":"girl tone 2","shortname":":girl_tone2:","category":"people","emoji_order":"107","aliases":[],"aliases_ascii":[],"keywords":[]},"girl_tone3":{"unicode":"1f467-1f3fd","unicode_alt":"","code_decimal":"👧🏽","name":"girl tone 3","shortname":":girl_tone3:","category":"people","emoji_order":"108","aliases":[],"aliases_ascii":[],"keywords":[]},"girl_tone4":{"unicode":"1f467-1f3fe","unicode_alt":"","code_decimal":"👧🏾","name":"girl tone 4","shortname":":girl_tone4:","category":"people","emoji_order":"109","aliases":[],"aliases_ascii":[],"keywords":[]},"girl_tone5":{"unicode":"1f467-1f3ff","unicode_alt":"","code_decimal":"👧🏿","name":"girl tone 5","shortname":":girl_tone5:","category":"people","emoji_order":"110","aliases":[],"aliases_ascii":[],"keywords":[]},"man":{"unicode":"1f468","unicode_alt":"","code_decimal":"👨","name":"man","shortname":":man:","category":"people","emoji_order":"111","aliases":[],"aliases_ascii":[],"keywords":["people","men","sex","diversity","selfie","boys night"]},"man_tone1":{"unicode":"1f468-1f3fb","unicode_alt":"","code_decimal":"👨🏻","name":"man tone 1","shortname":":man_tone1:","category":"people","emoji_order":"112","aliases":[],"aliases_ascii":[],"keywords":[]},"man_tone2":{"unicode":"1f468-1f3fc","unicode_alt":"","code_decimal":"👨🏼","name":"man tone 2","shortname":":man_tone2:","category":"people","emoji_order":"113","aliases":[],"aliases_ascii":[],"keywords":[]},"man_tone3":{"unicode":"1f468-1f3fd","unicode_alt":"","code_decimal":"👨🏽","name":"man tone 3","shortname":":man_tone3:","category":"people","emoji_order":"114","aliases":[],"aliases_ascii":[],"keywords":[]},"man_tone4":{"unicode":"1f468-1f3fe","unicode_alt":"","code_decimal":"👨🏾","name":"man tone 4","shortname":":man_tone4:","category":"people","emoji_order":"115","aliases":[],"aliases_ascii":[],"keywords":[]},"man_tone5":{"unicode":"1f468-1f3ff","unicode_alt":"","code_decimal":"👨🏿","name":"man tone 5","shortname":":man_tone5:","category":"people","emoji_order":"116","aliases":[],"aliases_ascii":[],"keywords":[]},"woman":{"unicode":"1f469","unicode_alt":"","code_decimal":"👩","name":"woman","shortname":":woman:","category":"people","emoji_order":"117","aliases":[],"aliases_ascii":[],"keywords":["people","women","sex","diversity","feminist","selfie","girls night"]},"woman_tone1":{"unicode":"1f469-1f3fb","unicode_alt":"","code_decimal":"👩🏻","name":"woman tone 1","shortname":":woman_tone1:","category":"people","emoji_order":"118","aliases":[],"aliases_ascii":[],"keywords":[]},"woman_tone2":{"unicode":"1f469-1f3fc","unicode_alt":"","code_decimal":"👩🏼","name":"woman tone 2","shortname":":woman_tone2:","category":"people","emoji_order":"119","aliases":[],"aliases_ascii":[],"keywords":[]},"woman_tone3":{"unicode":"1f469-1f3fd","unicode_alt":"","code_decimal":"👩🏽","name":"woman tone 3","shortname":":woman_tone3:","category":"people","emoji_order":"120","aliases":[],"aliases_ascii":[],"keywords":[]},"woman_tone4":{"unicode":"1f469-1f3fe","unicode_alt":"","code_decimal":"👩🏾","name":"woman tone 4","shortname":":woman_tone4:","category":"people","emoji_order":"121","aliases":[],"aliases_ascii":[],"keywords":[]},"woman_tone5":{"unicode":"1f469-1f3ff","unicode_alt":"","code_decimal":"👩🏿","name":"woman tone 5","shortname":":woman_tone5:","category":"people","emoji_order":"122","aliases":[],"aliases_ascii":[],"keywords":[]},"older_man":{"unicode":"1f474","unicode_alt":"","code_decimal":"👴","name":"older man","shortname":":older_man:","category":"people","emoji_order":"123","aliases":[],"aliases_ascii":[],"keywords":["people","men","old people","diversity"]},"older_man_tone1":{"unicode":"1f474-1f3fb","unicode_alt":"","code_decimal":"👴🏻","name":"older man tone 1","shortname":":older_man_tone1:","category":"people","emoji_order":"124","aliases":[],"aliases_ascii":[],"keywords":[]},"older_man_tone2":{"unicode":"1f474-1f3fc","unicode_alt":"","code_decimal":"👴🏼","name":"older man tone 2","shortname":":older_man_tone2:","category":"people","emoji_order":"125","aliases":[],"aliases_ascii":[],"keywords":[]},"older_man_tone3":{"unicode":"1f474-1f3fd","unicode_alt":"","code_decimal":"👴🏽","name":"older man tone 3","shortname":":older_man_tone3:","category":"people","emoji_order":"126","aliases":[],"aliases_ascii":[],"keywords":[]},"older_man_tone4":{"unicode":"1f474-1f3fe","unicode_alt":"","code_decimal":"👴🏾","name":"older man tone 4","shortname":":older_man_tone4:","category":"people","emoji_order":"127","aliases":[],"aliases_ascii":[],"keywords":[]},"older_man_tone5":{"unicode":"1f474-1f3ff","unicode_alt":"","code_decimal":"👴🏿","name":"older man tone 5","shortname":":older_man_tone5:","category":"people","emoji_order":"128","aliases":[],"aliases_ascii":[],"keywords":[]},"older_woman":{"unicode":"1f475","unicode_alt":"","code_decimal":"👵","name":"older woman","shortname":":older_woman:","category":"people","emoji_order":"129","aliases":[":grandma:"],"aliases_ascii":[],"keywords":["people","old people","diversity"]},"older_woman_tone1":{"unicode":"1f475-1f3fb","unicode_alt":"","code_decimal":"👵🏻","name":"older woman tone 1","shortname":":older_woman_tone1:","category":"people","emoji_order":"130","aliases":[":grandma_tone1:"],"aliases_ascii":[],"keywords":[]},"older_woman_tone2":{"unicode":"1f475-1f3fc","unicode_alt":"","code_decimal":"👵🏼","name":"older woman tone 2","shortname":":older_woman_tone2:","category":"people","emoji_order":"131","aliases":[":grandma_tone2:"],"aliases_ascii":[],"keywords":[]},"older_woman_tone3":{"unicode":"1f475-1f3fd","unicode_alt":"","code_decimal":"👵🏽","name":"older woman tone 3","shortname":":older_woman_tone3:","category":"people","emoji_order":"132","aliases":[":grandma_tone3:"],"aliases_ascii":[],"keywords":[]},"older_woman_tone4":{"unicode":"1f475-1f3fe","unicode_alt":"","code_decimal":"👵🏾","name":"older woman tone 4","shortname":":older_woman_tone4:","category":"people","emoji_order":"133","aliases":[":grandma_tone4:"],"aliases_ascii":[],"keywords":[]},"older_woman_tone5":{"unicode":"1f475-1f3ff","unicode_alt":"","code_decimal":"👵🏿","name":"older woman tone 5","shortname":":older_woman_tone5:","category":"people","emoji_order":"134","aliases":[":grandma_tone5:"],"aliases_ascii":[],"keywords":[]},"baby":{"unicode":"1f476","unicode_alt":"","code_decimal":"👶","name":"baby","shortname":":baby:","category":"people","emoji_order":"135","aliases":[],"aliases_ascii":[],"keywords":["people","baby","diversity"]},"baby_tone1":{"unicode":"1f476-1f3fb","unicode_alt":"","code_decimal":"👶🏻","name":"baby tone 1","shortname":":baby_tone1:","category":"people","emoji_order":"136","aliases":[],"aliases_ascii":[],"keywords":[]},"baby_tone2":{"unicode":"1f476-1f3fc","unicode_alt":"","code_decimal":"👶🏼","name":"baby tone 2","shortname":":baby_tone2:","category":"people","emoji_order":"137","aliases":[],"aliases_ascii":[],"keywords":[]},"baby_tone3":{"unicode":"1f476-1f3fd","unicode_alt":"","code_decimal":"👶🏽","name":"baby tone 3","shortname":":baby_tone3:","category":"people","emoji_order":"138","aliases":[],"aliases_ascii":[],"keywords":[]},"baby_tone4":{"unicode":"1f476-1f3fe","unicode_alt":"","code_decimal":"👶🏾","name":"baby tone 4","shortname":":baby_tone4:","category":"people","emoji_order":"139","aliases":[],"aliases_ascii":[],"keywords":[]},"baby_tone5":{"unicode":"1f476-1f3ff","unicode_alt":"","code_decimal":"👶🏿","name":"baby tone 5","shortname":":baby_tone5:","category":"people","emoji_order":"140","aliases":[],"aliases_ascii":[],"keywords":[]},"angel":{"unicode":"1f47c","unicode_alt":"","code_decimal":"👼","name":"baby angel","shortname":":angel:","category":"people","emoji_order":"141","aliases":[],"aliases_ascii":[],"keywords":["people","diversity","omg"]},"angel_tone1":{"unicode":"1f47c-1f3fb","unicode_alt":"","code_decimal":"👼🏻","name":"baby angel tone 1","shortname":":angel_tone1:","category":"people","emoji_order":"142","aliases":[],"aliases_ascii":[],"keywords":[]},"angel_tone2":{"unicode":"1f47c-1f3fc","unicode_alt":"","code_decimal":"👼🏼","name":"baby angel tone 2","shortname":":angel_tone2:","category":"people","emoji_order":"143","aliases":[],"aliases_ascii":[],"keywords":[]},"angel_tone3":{"unicode":"1f47c-1f3fd","unicode_alt":"","code_decimal":"👼🏽","name":"baby angel tone 3","shortname":":angel_tone3:","category":"people","emoji_order":"144","aliases":[],"aliases_ascii":[],"keywords":[]},"angel_tone4":{"unicode":"1f47c-1f3fe","unicode_alt":"","code_decimal":"👼🏾","name":"baby angel tone 4","shortname":":angel_tone4:","category":"people","emoji_order":"145","aliases":[],"aliases_ascii":[],"keywords":[]},"angel_tone5":{"unicode":"1f47c-1f3ff","unicode_alt":"","code_decimal":"👼🏿","name":"baby angel tone 5","shortname":":angel_tone5:","category":"people","emoji_order":"146","aliases":[],"aliases_ascii":[],"keywords":[]},"cop":{"unicode":"1f46e","unicode_alt":"","code_decimal":"👮","name":"police officer","shortname":":cop:","category":"people","emoji_order":"339","aliases":[],"aliases_ascii":[],"keywords":["people","hat","men","diversity","job","police","911"]},"cop_tone1":{"unicode":"1f46e-1f3fb","unicode_alt":"","code_decimal":"👮🏻","name":"police officer tone 1","shortname":":cop_tone1:","category":"people","emoji_order":"340","aliases":[],"aliases_ascii":[],"keywords":[]},"cop_tone2":{"unicode":"1f46e-1f3fc","unicode_alt":"","code_decimal":"👮🏼","name":"police officer tone 2","shortname":":cop_tone2:","category":"people","emoji_order":"341","aliases":[],"aliases_ascii":[],"keywords":[]},"cop_tone3":{"unicode":"1f46e-1f3fd","unicode_alt":"","code_decimal":"👮🏽","name":"police officer tone 3","shortname":":cop_tone3:","category":"people","emoji_order":"342","aliases":[],"aliases_ascii":[],"keywords":[]},"cop_tone4":{"unicode":"1f46e-1f3fe","unicode_alt":"","code_decimal":"👮🏾","name":"police officer tone 4","shortname":":cop_tone4:","category":"people","emoji_order":"343","aliases":[],"aliases_ascii":[],"keywords":[]},"cop_tone5":{"unicode":"1f46e-1f3ff","unicode_alt":"","code_decimal":"👮🏿","name":"police officer tone 5","shortname":":cop_tone5:","category":"people","emoji_order":"344","aliases":[],"aliases_ascii":[],"keywords":[]},"spy":{"unicode":"1f575","unicode_alt":"1f575-fe0f","code_decimal":"🕵","name":"sleuth or spy","shortname":":spy:","category":"people","emoji_order":"357","aliases":[":sleuth_or_spy:"],"aliases_ascii":[],"keywords":["people","hat","men","glasses","diversity","job"]},"spy_tone1":{"unicode":"1f575-1f3fb","unicode_alt":"","code_decimal":"🕵🏻","name":"sleuth or spy tone 1","shortname":":spy_tone1:","category":"people","emoji_order":"358","aliases":[":sleuth_or_spy_tone1:"],"aliases_ascii":[],"keywords":[]},"spy_tone2":{"unicode":"1f575-1f3fc","unicode_alt":"","code_decimal":"🕵🏼","name":"sleuth or spy tone 2","shortname":":spy_tone2:","category":"people","emoji_order":"359","aliases":[":sleuth_or_spy_tone2:"],"aliases_ascii":[],"keywords":[]},"spy_tone3":{"unicode":"1f575-1f3fd","unicode_alt":"","code_decimal":"🕵🏽","name":"sleuth or spy tone 3","shortname":":spy_tone3:","category":"people","emoji_order":"360","aliases":[":sleuth_or_spy_tone3:"],"aliases_ascii":[],"keywords":[]},"spy_tone4":{"unicode":"1f575-1f3fe","unicode_alt":"","code_decimal":"🕵🏾","name":"sleuth or spy tone 4","shortname":":spy_tone4:","category":"people","emoji_order":"361","aliases":[":sleuth_or_spy_tone4:"],"aliases_ascii":[],"keywords":[]},"spy_tone5":{"unicode":"1f575-1f3ff","unicode_alt":"","code_decimal":"🕵🏿","name":"sleuth or spy tone 5","shortname":":spy_tone5:","category":"people","emoji_order":"362","aliases":[":sleuth_or_spy_tone5:"],"aliases_ascii":[],"keywords":[]},"guardsman":{"unicode":"1f482","unicode_alt":"","code_decimal":"💂","name":"guardsman","shortname":":guardsman:","category":"people","emoji_order":"375","aliases":[],"aliases_ascii":[],"keywords":["people","hat","men","diversity","job"]},"guardsman_tone1":{"unicode":"1f482-1f3fb","unicode_alt":"","code_decimal":"💂🏻","name":"guardsman tone 1","shortname":":guardsman_tone1:","category":"people","emoji_order":"376","aliases":[],"aliases_ascii":[],"keywords":[]},"guardsman_tone2":{"unicode":"1f482-1f3fc","unicode_alt":"","code_decimal":"💂🏼","name":"guardsman tone 2","shortname":":guardsman_tone2:","category":"people","emoji_order":"377","aliases":[],"aliases_ascii":[],"keywords":[]},"guardsman_tone3":{"unicode":"1f482-1f3fd","unicode_alt":"","code_decimal":"💂🏽","name":"guardsman tone 3","shortname":":guardsman_tone3:","category":"people","emoji_order":"378","aliases":[],"aliases_ascii":[],"keywords":[]},"guardsman_tone4":{"unicode":"1f482-1f3fe","unicode_alt":"","code_decimal":"💂🏾","name":"guardsman tone 4","shortname":":guardsman_tone4:","category":"people","emoji_order":"379","aliases":[],"aliases_ascii":[],"keywords":[]},"guardsman_tone5":{"unicode":"1f482-1f3ff","unicode_alt":"","code_decimal":"💂🏿","name":"guardsman tone 5","shortname":":guardsman_tone5:","category":"people","emoji_order":"380","aliases":[],"aliases_ascii":[],"keywords":[]},"construction_worker":{"unicode":"1f477","unicode_alt":"","code_decimal":"👷","name":"construction worker","shortname":":construction_worker:","category":"people","emoji_order":"393","aliases":[],"aliases_ascii":[],"keywords":["people","hat","men","diversity","job"]},"construction_worker_tone1":{"unicode":"1f477-1f3fb","unicode_alt":"","code_decimal":"👷🏻","name":"construction worker tone 1","shortname":":construction_worker_tone1:","category":"people","emoji_order":"394","aliases":[],"aliases_ascii":[],"keywords":[]},"construction_worker_tone2":{"unicode":"1f477-1f3fc","unicode_alt":"","code_decimal":"👷🏼","name":"construction worker tone 2","shortname":":construction_worker_tone2:","category":"people","emoji_order":"395","aliases":[],"aliases_ascii":[],"keywords":[]},"construction_worker_tone3":{"unicode":"1f477-1f3fd","unicode_alt":"","code_decimal":"👷🏽","name":"construction worker tone 3","shortname":":construction_worker_tone3:","category":"people","emoji_order":"396","aliases":[],"aliases_ascii":[],"keywords":[]},"construction_worker_tone4":{"unicode":"1f477-1f3fe","unicode_alt":"","code_decimal":"👷🏾","name":"construction worker tone 4","shortname":":construction_worker_tone4:","category":"people","emoji_order":"397","aliases":[],"aliases_ascii":[],"keywords":[]},"construction_worker_tone5":{"unicode":"1f477-1f3ff","unicode_alt":"","code_decimal":"👷🏿","name":"construction worker tone 5","shortname":":construction_worker_tone5:","category":"people","emoji_order":"398","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_turban":{"unicode":"1f473","unicode_alt":"","code_decimal":"👳","name":"man with turban","shortname":":man_with_turban:","category":"people","emoji_order":"411","aliases":[],"aliases_ascii":[],"keywords":["people","hat","diversity"]},"man_with_turban_tone1":{"unicode":"1f473-1f3fb","unicode_alt":"","code_decimal":"👳🏻","name":"man with turban tone 1","shortname":":man_with_turban_tone1:","category":"people","emoji_order":"412","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_turban_tone2":{"unicode":"1f473-1f3fc","unicode_alt":"","code_decimal":"👳🏼","name":"man with turban tone 2","shortname":":man_with_turban_tone2:","category":"people","emoji_order":"413","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_turban_tone3":{"unicode":"1f473-1f3fd","unicode_alt":"","code_decimal":"👳🏽","name":"man with turban tone 3","shortname":":man_with_turban_tone3:","category":"people","emoji_order":"414","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_turban_tone4":{"unicode":"1f473-1f3fe","unicode_alt":"","code_decimal":"👳🏾","name":"man with turban tone 4","shortname":":man_with_turban_tone4:","category":"people","emoji_order":"415","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_turban_tone5":{"unicode":"1f473-1f3ff","unicode_alt":"","code_decimal":"👳🏿","name":"man with turban tone 5","shortname":":man_with_turban_tone5:","category":"people","emoji_order":"416","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_blond_hair":{"unicode":"1f471","unicode_alt":"","code_decimal":"👱","name":"person with blond hair","shortname":":person_with_blond_hair:","category":"people","emoji_order":"429","aliases":[],"aliases_ascii":[],"keywords":["people","men","diversity"]},"person_with_blond_hair_tone1":{"unicode":"1f471-1f3fb","unicode_alt":"","code_decimal":"👱🏻","name":"person with blond hair tone 1","shortname":":person_with_blond_hair_tone1:","category":"people","emoji_order":"430","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_blond_hair_tone2":{"unicode":"1f471-1f3fc","unicode_alt":"","code_decimal":"👱🏼","name":"person with blond hair tone 2","shortname":":person_with_blond_hair_tone2:","category":"people","emoji_order":"431","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_blond_hair_tone3":{"unicode":"1f471-1f3fd","unicode_alt":"","code_decimal":"👱🏽","name":"person with blond hair tone 3","shortname":":person_with_blond_hair_tone3:","category":"people","emoji_order":"432","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_blond_hair_tone4":{"unicode":"1f471-1f3fe","unicode_alt":"","code_decimal":"👱🏾","name":"person with blond hair tone 4","shortname":":person_with_blond_hair_tone4:","category":"people","emoji_order":"433","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_blond_hair_tone5":{"unicode":"1f471-1f3ff","unicode_alt":"","code_decimal":"👱🏿","name":"person with blond hair tone 5","shortname":":person_with_blond_hair_tone5:","category":"people","emoji_order":"434","aliases":[],"aliases_ascii":[],"keywords":[]},"santa":{"unicode":"1f385","unicode_alt":"","code_decimal":"🎅","name":"father christmas","shortname":":santa:","category":"people","emoji_order":"447","aliases":[],"aliases_ascii":[],"keywords":["people","hat","winter","holidays","christmas","diversity","santa"]},"santa_tone1":{"unicode":"1f385-1f3fb","unicode_alt":"","code_decimal":"🎅🏻","name":"father christmas tone 1","shortname":":santa_tone1:","category":"people","emoji_order":"448","aliases":[],"aliases_ascii":[],"keywords":[]},"santa_tone2":{"unicode":"1f385-1f3fc","unicode_alt":"","code_decimal":"🎅🏼","name":"father christmas tone 2","shortname":":santa_tone2:","category":"people","emoji_order":"449","aliases":[],"aliases_ascii":[],"keywords":[]},"santa_tone3":{"unicode":"1f385-1f3fd","unicode_alt":"","code_decimal":"🎅🏽","name":"father christmas tone 3","shortname":":santa_tone3:","category":"people","emoji_order":"450","aliases":[],"aliases_ascii":[],"keywords":[]},"santa_tone4":{"unicode":"1f385-1f3fe","unicode_alt":"","code_decimal":"🎅🏾","name":"father christmas tone 4","shortname":":santa_tone4:","category":"people","emoji_order":"451","aliases":[],"aliases_ascii":[],"keywords":[]},"santa_tone5":{"unicode":"1f385-1f3ff","unicode_alt":"","code_decimal":"🎅🏿","name":"father christmas tone 5","shortname":":santa_tone5:","category":"people","emoji_order":"452","aliases":[],"aliases_ascii":[],"keywords":[]},"mrs_claus":{"unicode":"1f936","unicode_alt":"","code_decimal":"🤶","name":"mother christmas","shortname":":mrs_claus:","category":"people","emoji_order":"453","aliases":[":mother_christmas:"],"aliases_ascii":[],"keywords":[]},"mrs_claus_tone1":{"unicode":"1f936-1f3fb","unicode_alt":"","code_decimal":"🤶🏻","name":"mother christmas tone 1","shortname":":mrs_claus_tone1:","category":"people","emoji_order":"454","aliases":[":mother_christmas_tone1:"],"aliases_ascii":[],"keywords":[]},"mrs_claus_tone2":{"unicode":"1f936-1f3fc","unicode_alt":"","code_decimal":"🤶🏼","name":"mother christmas tone 2","shortname":":mrs_claus_tone2:","category":"people","emoji_order":"455","aliases":[":mother_christmas_tone2:"],"aliases_ascii":[],"keywords":[]},"mrs_claus_tone3":{"unicode":"1f936-1f3fd","unicode_alt":"","code_decimal":"🤶🏽","name":"mother christmas tone 3","shortname":":mrs_claus_tone3:","category":"people","emoji_order":"456","aliases":[":mother_christmas_tone3:"],"aliases_ascii":[],"keywords":[]},"mrs_claus_tone4":{"unicode":"1f936-1f3fe","unicode_alt":"","code_decimal":"🤶🏾","name":"mother christmas tone 4","shortname":":mrs_claus_tone4:","category":"people","emoji_order":"457","aliases":[":mother_christmas_tone4:"],"aliases_ascii":[],"keywords":[]},"mrs_claus_tone5":{"unicode":"1f936-1f3ff","unicode_alt":"","code_decimal":"🤶🏿","name":"mother christmas tone 5","shortname":":mrs_claus_tone5:","category":"people","emoji_order":"458","aliases":[":mother_christmas_tone5:"],"aliases_ascii":[],"keywords":[]},"princess":{"unicode":"1f478","unicode_alt":"","code_decimal":"👸","name":"princess","shortname":":princess:","category":"people","emoji_order":"459","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity","beautiful","girls night"]},"princess_tone1":{"unicode":"1f478-1f3fb","unicode_alt":"","code_decimal":"👸🏻","name":"princess tone 1","shortname":":princess_tone1:","category":"people","emoji_order":"460","aliases":[],"aliases_ascii":[],"keywords":[]},"princess_tone2":{"unicode":"1f478-1f3fc","unicode_alt":"","code_decimal":"👸🏼","name":"princess tone 2","shortname":":princess_tone2:","category":"people","emoji_order":"461","aliases":[],"aliases_ascii":[],"keywords":[]},"princess_tone3":{"unicode":"1f478-1f3fd","unicode_alt":"","code_decimal":"👸🏽","name":"princess tone 3","shortname":":princess_tone3:","category":"people","emoji_order":"462","aliases":[],"aliases_ascii":[],"keywords":[]},"princess_tone4":{"unicode":"1f478-1f3fe","unicode_alt":"","code_decimal":"👸🏾","name":"princess tone 4","shortname":":princess_tone4:","category":"people","emoji_order":"463","aliases":[],"aliases_ascii":[],"keywords":[]},"princess_tone5":{"unicode":"1f478-1f3ff","unicode_alt":"","code_decimal":"👸🏿","name":"princess tone 5","shortname":":princess_tone5:","category":"people","emoji_order":"464","aliases":[],"aliases_ascii":[],"keywords":[]},"prince":{"unicode":"1f934","unicode_alt":"","code_decimal":"🤴","name":"prince","shortname":":prince:","category":"people","emoji_order":"465","aliases":[],"aliases_ascii":[],"keywords":[]},"prince_tone1":{"unicode":"1f934-1f3fb","unicode_alt":"","code_decimal":"🤴🏻","name":"prince tone 1","shortname":":prince_tone1:","category":"people","emoji_order":"466","aliases":[],"aliases_ascii":[],"keywords":[]},"prince_tone2":{"unicode":"1f934-1f3fc","unicode_alt":"","code_decimal":"🤴🏼","name":"prince tone 2","shortname":":prince_tone2:","category":"people","emoji_order":"467","aliases":[],"aliases_ascii":[],"keywords":[]},"prince_tone3":{"unicode":"1f934-1f3fd","unicode_alt":"","code_decimal":"🤴🏽","name":"prince tone 3","shortname":":prince_tone3:","category":"people","emoji_order":"468","aliases":[],"aliases_ascii":[],"keywords":[]},"prince_tone4":{"unicode":"1f934-1f3fe","unicode_alt":"","code_decimal":"🤴🏾","name":"prince tone 4","shortname":":prince_tone4:","category":"people","emoji_order":"469","aliases":[],"aliases_ascii":[],"keywords":[]},"prince_tone5":{"unicode":"1f934-1f3ff","unicode_alt":"","code_decimal":"🤴🏿","name":"prince tone 5","shortname":":prince_tone5:","category":"people","emoji_order":"470","aliases":[],"aliases_ascii":[],"keywords":[]},"bride_with_veil":{"unicode":"1f470","unicode_alt":"","code_decimal":"👰","name":"bride with veil","shortname":":bride_with_veil:","category":"people","emoji_order":"471","aliases":[],"aliases_ascii":[],"keywords":["people","wedding","women","diversity"]},"bride_with_veil_tone1":{"unicode":"1f470-1f3fb","unicode_alt":"","code_decimal":"👰🏻","name":"bride with veil tone 1","shortname":":bride_with_veil_tone1:","category":"people","emoji_order":"472","aliases":[],"aliases_ascii":[],"keywords":[]},"bride_with_veil_tone2":{"unicode":"1f470-1f3fc","unicode_alt":"","code_decimal":"👰🏼","name":"bride with veil tone 2","shortname":":bride_with_veil_tone2:","category":"people","emoji_order":"473","aliases":[],"aliases_ascii":[],"keywords":[]},"bride_with_veil_tone3":{"unicode":"1f470-1f3fd","unicode_alt":"","code_decimal":"👰🏽","name":"bride with veil tone 3","shortname":":bride_with_veil_tone3:","category":"people","emoji_order":"474","aliases":[],"aliases_ascii":[],"keywords":[]},"bride_with_veil_tone4":{"unicode":"1f470-1f3fe","unicode_alt":"","code_decimal":"👰🏾","name":"bride with veil tone 4","shortname":":bride_with_veil_tone4:","category":"people","emoji_order":"475","aliases":[],"aliases_ascii":[],"keywords":[]},"bride_with_veil_tone5":{"unicode":"1f470-1f3ff","unicode_alt":"","code_decimal":"👰🏿","name":"bride with veil tone 5","shortname":":bride_with_veil_tone5:","category":"people","emoji_order":"476","aliases":[],"aliases_ascii":[],"keywords":[]},"man_in_tuxedo":{"unicode":"1f935","unicode_alt":"","code_decimal":"🤵","name":"man in tuxedo","shortname":":man_in_tuxedo:","category":"people","emoji_order":"477","aliases":[],"aliases_ascii":[],"keywords":[]},"man_in_tuxedo_tone1":{"unicode":"1f935-1f3fb","unicode_alt":"","code_decimal":"🤵🏻","name":"man in tuxedo tone 1","shortname":":man_in_tuxedo_tone1:","category":"people","emoji_order":"478","aliases":[":tuxedo_tone1:"],"aliases_ascii":[],"keywords":[]},"man_in_tuxedo_tone2":{"unicode":"1f935-1f3fc","unicode_alt":"","code_decimal":"🤵🏼","name":"man in tuxedo tone 2","shortname":":man_in_tuxedo_tone2:","category":"people","emoji_order":"479","aliases":[":tuxedo_tone2:"],"aliases_ascii":[],"keywords":[]},"man_in_tuxedo_tone3":{"unicode":"1f935-1f3fd","unicode_alt":"","code_decimal":"🤵🏽","name":"man in tuxedo tone 3","shortname":":man_in_tuxedo_tone3:","category":"people","emoji_order":"480","aliases":[":tuxedo_tone3:"],"aliases_ascii":[],"keywords":[]},"man_in_tuxedo_tone4":{"unicode":"1f935-1f3fe","unicode_alt":"","code_decimal":"🤵🏾","name":"man in tuxedo tone 4","shortname":":man_in_tuxedo_tone4:","category":"people","emoji_order":"481","aliases":[":tuxedo_tone4:"],"aliases_ascii":[],"keywords":[]},"man_in_tuxedo_tone5":{"unicode":"1f935-1f3ff","unicode_alt":"","code_decimal":"🤵🏿","name":"man in tuxedo tone 5","shortname":":man_in_tuxedo_tone5:","category":"people","emoji_order":"482","aliases":[":tuxedo_tone5:"],"aliases_ascii":[],"keywords":[]},"pregnant_woman":{"unicode":"1f930","unicode_alt":"","code_decimal":"🤰","name":"pregnant woman","shortname":":pregnant_woman:","category":"people","emoji_order":"483","aliases":[":expecting_woman:"],"aliases_ascii":[],"keywords":[]},"pregnant_woman_tone1":{"unicode":"1f930-1f3fb","unicode_alt":"","code_decimal":"🤰🏻","name":"pregnant woman tone 1","shortname":":pregnant_woman_tone1:","category":"people","emoji_order":"484","aliases":[":expecting_woman_tone1:"],"aliases_ascii":[],"keywords":[]},"pregnant_woman_tone2":{"unicode":"1f930-1f3fc","unicode_alt":"","code_decimal":"🤰🏼","name":"pregnant woman tone 2","shortname":":pregnant_woman_tone2:","category":"people","emoji_order":"485","aliases":[":expecting_woman_tone2:"],"aliases_ascii":[],"keywords":[]},"pregnant_woman_tone3":{"unicode":"1f930-1f3fd","unicode_alt":"","code_decimal":"🤰🏽","name":"pregnant woman tone 3","shortname":":pregnant_woman_tone3:","category":"people","emoji_order":"486","aliases":[":expecting_woman_tone3:"],"aliases_ascii":[],"keywords":[]},"pregnant_woman_tone4":{"unicode":"1f930-1f3fe","unicode_alt":"","code_decimal":"🤰🏾","name":"pregnant woman tone 4","shortname":":pregnant_woman_tone4:","category":"people","emoji_order":"487","aliases":[":expecting_woman_tone4:"],"aliases_ascii":[],"keywords":[]},"pregnant_woman_tone5":{"unicode":"1f930-1f3ff","unicode_alt":"","code_decimal":"🤰🏿","name":"pregnant woman tone 5","shortname":":pregnant_woman_tone5:","category":"people","emoji_order":"488","aliases":[":expecting_woman_tone5:"],"aliases_ascii":[],"keywords":[]},"man_with_gua_pi_mao":{"unicode":"1f472","unicode_alt":"","code_decimal":"👲","name":"man with gua pi mao","shortname":":man_with_gua_pi_mao:","category":"people","emoji_order":"489","aliases":[],"aliases_ascii":[],"keywords":["people","hat","men","diversity"]},"man_with_gua_pi_mao_tone1":{"unicode":"1f472-1f3fb","unicode_alt":"","code_decimal":"👲🏻","name":"man with gua pi mao tone 1","shortname":":man_with_gua_pi_mao_tone1:","category":"people","emoji_order":"490","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_gua_pi_mao_tone2":{"unicode":"1f472-1f3fc","unicode_alt":"","code_decimal":"👲🏼","name":"man with gua pi mao tone 2","shortname":":man_with_gua_pi_mao_tone2:","category":"people","emoji_order":"491","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_gua_pi_mao_tone3":{"unicode":"1f472-1f3fd","unicode_alt":"","code_decimal":"👲🏽","name":"man with gua pi mao tone 3","shortname":":man_with_gua_pi_mao_tone3:","category":"people","emoji_order":"492","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_gua_pi_mao_tone4":{"unicode":"1f472-1f3fe","unicode_alt":"","code_decimal":"👲🏾","name":"man with gua pi mao tone 4","shortname":":man_with_gua_pi_mao_tone4:","category":"people","emoji_order":"493","aliases":[],"aliases_ascii":[],"keywords":[]},"man_with_gua_pi_mao_tone5":{"unicode":"1f472-1f3ff","unicode_alt":"","code_decimal":"👲🏿","name":"man with gua pi mao tone 5","shortname":":man_with_gua_pi_mao_tone5:","category":"people","emoji_order":"494","aliases":[],"aliases_ascii":[],"keywords":[]},"person_frowning":{"unicode":"1f64d","unicode_alt":"","code_decimal":"🙍","name":"person frowning","shortname":":person_frowning:","category":"people","emoji_order":"495","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity"]},"person_frowning_tone1":{"unicode":"1f64d-1f3fb","unicode_alt":"","code_decimal":"🙍🏻","name":"person frowning tone 1","shortname":":person_frowning_tone1:","category":"people","emoji_order":"496","aliases":[],"aliases_ascii":[],"keywords":[]},"person_frowning_tone2":{"unicode":"1f64d-1f3fc","unicode_alt":"","code_decimal":"🙍🏼","name":"person frowning tone 2","shortname":":person_frowning_tone2:","category":"people","emoji_order":"497","aliases":[],"aliases_ascii":[],"keywords":[]},"person_frowning_tone3":{"unicode":"1f64d-1f3fd","unicode_alt":"","code_decimal":"🙍🏽","name":"person frowning tone 3","shortname":":person_frowning_tone3:","category":"people","emoji_order":"498","aliases":[],"aliases_ascii":[],"keywords":[]},"person_frowning_tone4":{"unicode":"1f64d-1f3fe","unicode_alt":"","code_decimal":"🙍🏾","name":"person frowning tone 4","shortname":":person_frowning_tone4:","category":"people","emoji_order":"499","aliases":[],"aliases_ascii":[],"keywords":[]},"person_frowning_tone5":{"unicode":"1f64d-1f3ff","unicode_alt":"","code_decimal":"🙍🏿","name":"person frowning tone 5","shortname":":person_frowning_tone5:","category":"people","emoji_order":"500","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_pouting_face":{"unicode":"1f64e","unicode_alt":"","code_decimal":"🙎","name":"person with pouting face","shortname":":person_with_pouting_face:","category":"people","emoji_order":"513","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity"]},"person_with_pouting_face_tone1":{"unicode":"1f64e-1f3fb","unicode_alt":"","code_decimal":"🙎🏻","name":"person with pouting face tone1","shortname":":person_with_pouting_face_tone1:","category":"people","emoji_order":"514","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_pouting_face_tone2":{"unicode":"1f64e-1f3fc","unicode_alt":"","code_decimal":"🙎🏼","name":"person with pouting face tone2","shortname":":person_with_pouting_face_tone2:","category":"people","emoji_order":"515","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_pouting_face_tone3":{"unicode":"1f64e-1f3fd","unicode_alt":"","code_decimal":"🙎🏽","name":"person with pouting face tone3","shortname":":person_with_pouting_face_tone3:","category":"people","emoji_order":"516","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_pouting_face_tone4":{"unicode":"1f64e-1f3fe","unicode_alt":"","code_decimal":"🙎🏾","name":"person with pouting face tone4","shortname":":person_with_pouting_face_tone4:","category":"people","emoji_order":"517","aliases":[],"aliases_ascii":[],"keywords":[]},"person_with_pouting_face_tone5":{"unicode":"1f64e-1f3ff","unicode_alt":"","code_decimal":"🙎🏿","name":"person with pouting face tone5","shortname":":person_with_pouting_face_tone5:","category":"people","emoji_order":"518","aliases":[],"aliases_ascii":[],"keywords":[]},"no_good":{"unicode":"1f645","unicode_alt":"","code_decimal":"🙅","name":"face with no good gesture","shortname":":no_good:","category":"people","emoji_order":"531","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity","girls night"]},"no_good_tone1":{"unicode":"1f645-1f3fb","unicode_alt":"","code_decimal":"🙅🏻","name":"face with no good gesture tone 1","shortname":":no_good_tone1:","category":"people","emoji_order":"532","aliases":[],"aliases_ascii":[],"keywords":[]},"no_good_tone2":{"unicode":"1f645-1f3fc","unicode_alt":"","code_decimal":"🙅🏼","name":"face with no good gesture tone 2","shortname":":no_good_tone2:","category":"people","emoji_order":"533","aliases":[],"aliases_ascii":[],"keywords":[]},"no_good_tone3":{"unicode":"1f645-1f3fd","unicode_alt":"","code_decimal":"🙅🏽","name":"face with no good gesture tone 3","shortname":":no_good_tone3:","category":"people","emoji_order":"534","aliases":[],"aliases_ascii":[],"keywords":[]},"no_good_tone4":{"unicode":"1f645-1f3fe","unicode_alt":"","code_decimal":"🙅🏾","name":"face with no good gesture tone 4","shortname":":no_good_tone4:","category":"people","emoji_order":"535","aliases":[],"aliases_ascii":[],"keywords":[]},"no_good_tone5":{"unicode":"1f645-1f3ff","unicode_alt":"","code_decimal":"🙅🏿","name":"face with no good gesture tone 5","shortname":":no_good_tone5:","category":"people","emoji_order":"536","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_woman":{"unicode":"1f646","unicode_alt":"","code_decimal":"🙆","name":"face with ok gesture","shortname":":ok_woman:","category":"people","emoji_order":"549","aliases":[],"aliases_ascii":["*\\0\/*","\\0\/","*\\O\/*","\\O\/"],"keywords":["people","women","diversity"]},"ok_woman_tone1":{"unicode":"1f646-1f3fb","unicode_alt":"","code_decimal":"🙆🏻","name":"face with ok gesture tone1","shortname":":ok_woman_tone1:","category":"people","emoji_order":"550","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_woman_tone2":{"unicode":"1f646-1f3fc","unicode_alt":"","code_decimal":"🙆🏼","name":"face with ok gesture tone2","shortname":":ok_woman_tone2:","category":"people","emoji_order":"551","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_woman_tone3":{"unicode":"1f646-1f3fd","unicode_alt":"","code_decimal":"🙆🏽","name":"face with ok gesture tone3","shortname":":ok_woman_tone3:","category":"people","emoji_order":"552","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_woman_tone4":{"unicode":"1f646-1f3fe","unicode_alt":"","code_decimal":"🙆🏾","name":"face with ok gesture tone4","shortname":":ok_woman_tone4:","category":"people","emoji_order":"553","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_woman_tone5":{"unicode":"1f646-1f3ff","unicode_alt":"","code_decimal":"🙆🏿","name":"face with ok gesture tone5","shortname":":ok_woman_tone5:","category":"people","emoji_order":"554","aliases":[],"aliases_ascii":[],"keywords":[]},"information_desk_person":{"unicode":"1f481","unicode_alt":"","code_decimal":"💁","name":"information desk person","shortname":":information_desk_person:","category":"people","emoji_order":"567","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity"]},"information_desk_person_tone1":{"unicode":"1f481-1f3fb","unicode_alt":"","code_decimal":"💁🏻","name":"information desk person tone 1","shortname":":information_desk_person_tone1:","category":"people","emoji_order":"568","aliases":[],"aliases_ascii":[],"keywords":[]},"information_desk_person_tone2":{"unicode":"1f481-1f3fc","unicode_alt":"","code_decimal":"💁🏼","name":"information desk person tone 2","shortname":":information_desk_person_tone2:","category":"people","emoji_order":"569","aliases":[],"aliases_ascii":[],"keywords":[]},"information_desk_person_tone3":{"unicode":"1f481-1f3fd","unicode_alt":"","code_decimal":"💁🏽","name":"information desk person tone 3","shortname":":information_desk_person_tone3:","category":"people","emoji_order":"570","aliases":[],"aliases_ascii":[],"keywords":[]},"information_desk_person_tone4":{"unicode":"1f481-1f3fe","unicode_alt":"","code_decimal":"💁🏾","name":"information desk person tone 4","shortname":":information_desk_person_tone4:","category":"people","emoji_order":"571","aliases":[],"aliases_ascii":[],"keywords":[]},"information_desk_person_tone5":{"unicode":"1f481-1f3ff","unicode_alt":"","code_decimal":"💁🏿","name":"information desk person tone 5","shortname":":information_desk_person_tone5:","category":"people","emoji_order":"572","aliases":[],"aliases_ascii":[],"keywords":[]},"raising_hand":{"unicode":"1f64b","unicode_alt":"","code_decimal":"🙋","name":"happy person raising one hand","shortname":":raising_hand:","category":"people","emoji_order":"585","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity"]},"raising_hand_tone1":{"unicode":"1f64b-1f3fb","unicode_alt":"","code_decimal":"🙋🏻","name":"happy person raising one hand tone1","shortname":":raising_hand_tone1:","category":"people","emoji_order":"586","aliases":[],"aliases_ascii":[],"keywords":[]},"raising_hand_tone2":{"unicode":"1f64b-1f3fc","unicode_alt":"","code_decimal":"🙋🏼","name":"happy person raising one hand tone2","shortname":":raising_hand_tone2:","category":"people","emoji_order":"587","aliases":[],"aliases_ascii":[],"keywords":[]},"raising_hand_tone3":{"unicode":"1f64b-1f3fd","unicode_alt":"","code_decimal":"🙋🏽","name":"happy person raising one hand tone3","shortname":":raising_hand_tone3:","category":"people","emoji_order":"588","aliases":[],"aliases_ascii":[],"keywords":[]},"raising_hand_tone4":{"unicode":"1f64b-1f3fe","unicode_alt":"","code_decimal":"🙋🏾","name":"happy person raising one hand tone4","shortname":":raising_hand_tone4:","category":"people","emoji_order":"589","aliases":[],"aliases_ascii":[],"keywords":[]},"raising_hand_tone5":{"unicode":"1f64b-1f3ff","unicode_alt":"","code_decimal":"🙋🏿","name":"happy person raising one hand tone5","shortname":":raising_hand_tone5:","category":"people","emoji_order":"590","aliases":[],"aliases_ascii":[],"keywords":[]},"bow":{"unicode":"1f647","unicode_alt":"","code_decimal":"🙇","name":"person bowing deeply","shortname":":bow:","category":"people","emoji_order":"603","aliases":[],"aliases_ascii":[],"keywords":["people","pray","diversity"]},"bow_tone1":{"unicode":"1f647-1f3fb","unicode_alt":"","code_decimal":"🙇🏻","name":"person bowing deeply tone 1","shortname":":bow_tone1:","category":"people","emoji_order":"604","aliases":[],"aliases_ascii":[],"keywords":[]},"bow_tone2":{"unicode":"1f647-1f3fc","unicode_alt":"","code_decimal":"🙇🏼","name":"person bowing deeply tone 2","shortname":":bow_tone2:","category":"people","emoji_order":"605","aliases":[],"aliases_ascii":[],"keywords":[]},"bow_tone3":{"unicode":"1f647-1f3fd","unicode_alt":"","code_decimal":"🙇🏽","name":"person bowing deeply tone 3","shortname":":bow_tone3:","category":"people","emoji_order":"606","aliases":[],"aliases_ascii":[],"keywords":[]},"bow_tone4":{"unicode":"1f647-1f3fe","unicode_alt":"","code_decimal":"🙇🏾","name":"person bowing deeply tone 4","shortname":":bow_tone4:","category":"people","emoji_order":"607","aliases":[],"aliases_ascii":[],"keywords":[]},"bow_tone5":{"unicode":"1f647-1f3ff","unicode_alt":"","code_decimal":"🙇🏿","name":"person bowing deeply tone 5","shortname":":bow_tone5:","category":"people","emoji_order":"608","aliases":[],"aliases_ascii":[],"keywords":[]},"face_palm":{"unicode":"1f926","unicode_alt":"","code_decimal":"🤦","name":"face palm","shortname":":face_palm:","category":"people","emoji_order":"621","aliases":[":facepalm:"],"aliases_ascii":[],"keywords":[]},"face_palm_tone1":{"unicode":"1f926-1f3fb","unicode_alt":"","code_decimal":"🤦🏻","name":"face palm tone 1","shortname":":face_palm_tone1:","category":"people","emoji_order":"622","aliases":[":facepalm_tone1:"],"aliases_ascii":[],"keywords":[]},"face_palm_tone2":{"unicode":"1f926-1f3fc","unicode_alt":"","code_decimal":"🤦🏼","name":"face palm tone 2","shortname":":face_palm_tone2:","category":"people","emoji_order":"623","aliases":[":facepalm_tone2:"],"aliases_ascii":[],"keywords":[]},"face_palm_tone3":{"unicode":"1f926-1f3fd","unicode_alt":"","code_decimal":"🤦🏽","name":"face palm tone 3","shortname":":face_palm_tone3:","category":"people","emoji_order":"624","aliases":[":facepalm_tone3:"],"aliases_ascii":[],"keywords":[]},"face_palm_tone4":{"unicode":"1f926-1f3fe","unicode_alt":"","code_decimal":"🤦🏾","name":"face palm tone 4","shortname":":face_palm_tone4:","category":"people","emoji_order":"625","aliases":[":facepalm_tone4:"],"aliases_ascii":[],"keywords":[]},"face_palm_tone5":{"unicode":"1f926-1f3ff","unicode_alt":"","code_decimal":"🤦🏿","name":"face palm tone 5","shortname":":face_palm_tone5:","category":"people","emoji_order":"626","aliases":[":facepalm_tone5:"],"aliases_ascii":[],"keywords":[]},"shrug":{"unicode":"1f937","unicode_alt":"","code_decimal":"🤷","name":"shrug","shortname":":shrug:","category":"people","emoji_order":"639","aliases":[],"aliases_ascii":[],"keywords":[]},"shrug_tone1":{"unicode":"1f937-1f3fb","unicode_alt":"","code_decimal":"🤷🏻","name":"shrug tone 1","shortname":":shrug_tone1:","category":"people","emoji_order":"640","aliases":[],"aliases_ascii":[],"keywords":[]},"shrug_tone2":{"unicode":"1f937-1f3fc","unicode_alt":"","code_decimal":"🤷🏼","name":"shrug tone 2","shortname":":shrug_tone2:","category":"people","emoji_order":"641","aliases":[],"aliases_ascii":[],"keywords":[]},"shrug_tone3":{"unicode":"1f937-1f3fd","unicode_alt":"","code_decimal":"🤷🏽","name":"shrug tone 3","shortname":":shrug_tone3:","category":"people","emoji_order":"642","aliases":[],"aliases_ascii":[],"keywords":[]},"shrug_tone4":{"unicode":"1f937-1f3fe","unicode_alt":"","code_decimal":"🤷🏾","name":"shrug tone 4","shortname":":shrug_tone4:","category":"people","emoji_order":"643","aliases":[],"aliases_ascii":[],"keywords":[]},"shrug_tone5":{"unicode":"1f937-1f3ff","unicode_alt":"","code_decimal":"🤷🏿","name":"shrug tone 5","shortname":":shrug_tone5:","category":"people","emoji_order":"644","aliases":[],"aliases_ascii":[],"keywords":[]},"massage":{"unicode":"1f486","unicode_alt":"","code_decimal":"💆","name":"face massage","shortname":":massage:","category":"people","emoji_order":"657","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity"]},"massage_tone1":{"unicode":"1f486-1f3fb","unicode_alt":"","code_decimal":"💆🏻","name":"face massage tone 1","shortname":":massage_tone1:","category":"people","emoji_order":"658","aliases":[],"aliases_ascii":[],"keywords":[]},"massage_tone2":{"unicode":"1f486-1f3fc","unicode_alt":"","code_decimal":"💆🏼","name":"face massage tone 2","shortname":":massage_tone2:","category":"people","emoji_order":"659","aliases":[],"aliases_ascii":[],"keywords":[]},"massage_tone3":{"unicode":"1f486-1f3fd","unicode_alt":"","code_decimal":"💆🏽","name":"face massage tone 3","shortname":":massage_tone3:","category":"people","emoji_order":"660","aliases":[],"aliases_ascii":[],"keywords":[]},"massage_tone4":{"unicode":"1f486-1f3fe","unicode_alt":"","code_decimal":"💆🏾","name":"face massage tone 4","shortname":":massage_tone4:","category":"people","emoji_order":"661","aliases":[],"aliases_ascii":[],"keywords":[]},"massage_tone5":{"unicode":"1f486-1f3ff","unicode_alt":"","code_decimal":"💆🏿","name":"face massage tone 5","shortname":":massage_tone5:","category":"people","emoji_order":"662","aliases":[],"aliases_ascii":[],"keywords":[]},"haircut":{"unicode":"1f487","unicode_alt":"","code_decimal":"💇","name":"haircut","shortname":":haircut:","category":"people","emoji_order":"675","aliases":[],"aliases_ascii":[],"keywords":["people","women","diversity"]},"haircut_tone1":{"unicode":"1f487-1f3fb","unicode_alt":"","code_decimal":"💇🏻","name":"haircut tone 1","shortname":":haircut_tone1:","category":"people","emoji_order":"676","aliases":[],"aliases_ascii":[],"keywords":[]},"haircut_tone2":{"unicode":"1f487-1f3fc","unicode_alt":"","code_decimal":"💇🏼","name":"haircut tone 2","shortname":":haircut_tone2:","category":"people","emoji_order":"677","aliases":[],"aliases_ascii":[],"keywords":[]},"haircut_tone3":{"unicode":"1f487-1f3fd","unicode_alt":"","code_decimal":"💇🏽","name":"haircut tone 3","shortname":":haircut_tone3:","category":"people","emoji_order":"678","aliases":[],"aliases_ascii":[],"keywords":[]},"haircut_tone4":{"unicode":"1f487-1f3fe","unicode_alt":"","code_decimal":"💇🏾","name":"haircut tone 4","shortname":":haircut_tone4:","category":"people","emoji_order":"679","aliases":[],"aliases_ascii":[],"keywords":[]},"haircut_tone5":{"unicode":"1f487-1f3ff","unicode_alt":"","code_decimal":"💇🏿","name":"haircut tone 5","shortname":":haircut_tone5:","category":"people","emoji_order":"680","aliases":[],"aliases_ascii":[],"keywords":[]},"walking":{"unicode":"1f6b6","unicode_alt":"","code_decimal":"🚶","name":"pedestrian","shortname":":walking:","category":"people","emoji_order":"693","aliases":[],"aliases_ascii":[],"keywords":["people","men","diversity"]},"walking_tone1":{"unicode":"1f6b6-1f3fb","unicode_alt":"","code_decimal":"🚶🏻","name":"pedestrian tone 1","shortname":":walking_tone1:","category":"people","emoji_order":"694","aliases":[],"aliases_ascii":[],"keywords":[]},"walking_tone2":{"unicode":"1f6b6-1f3fc","unicode_alt":"","code_decimal":"🚶🏼","name":"pedestrian tone 2","shortname":":walking_tone2:","category":"people","emoji_order":"695","aliases":[],"aliases_ascii":[],"keywords":[]},"walking_tone3":{"unicode":"1f6b6-1f3fd","unicode_alt":"","code_decimal":"🚶🏽","name":"pedestrian tone 3","shortname":":walking_tone3:","category":"people","emoji_order":"696","aliases":[],"aliases_ascii":[],"keywords":[]},"walking_tone4":{"unicode":"1f6b6-1f3fe","unicode_alt":"","code_decimal":"🚶🏾","name":"pedestrian tone 4","shortname":":walking_tone4:","category":"people","emoji_order":"697","aliases":[],"aliases_ascii":[],"keywords":[]},"walking_tone5":{"unicode":"1f6b6-1f3ff","unicode_alt":"","code_decimal":"🚶🏿","name":"pedestrian tone 5","shortname":":walking_tone5:","category":"people","emoji_order":"698","aliases":[],"aliases_ascii":[],"keywords":[]},"runner":{"unicode":"1f3c3","unicode_alt":"","code_decimal":"🏃","name":"runner","shortname":":runner:","category":"people","emoji_order":"711","aliases":[],"aliases_ascii":[],"keywords":["people","men","diversity","boys night","run"]},"runner_tone1":{"unicode":"1f3c3-1f3fb","unicode_alt":"","code_decimal":"🏃🏻","name":"runner tone 1","shortname":":runner_tone1:","category":"people","emoji_order":"712","aliases":[],"aliases_ascii":[],"keywords":[]},"runner_tone2":{"unicode":"1f3c3-1f3fc","unicode_alt":"","code_decimal":"🏃🏼","name":"runner tone 2","shortname":":runner_tone2:","category":"people","emoji_order":"713","aliases":[],"aliases_ascii":[],"keywords":[]},"runner_tone3":{"unicode":"1f3c3-1f3fd","unicode_alt":"","code_decimal":"🏃🏽","name":"runner tone 3","shortname":":runner_tone3:","category":"people","emoji_order":"714","aliases":[],"aliases_ascii":[],"keywords":[]},"runner_tone4":{"unicode":"1f3c3-1f3fe","unicode_alt":"","code_decimal":"🏃🏾","name":"runner tone 4","shortname":":runner_tone4:","category":"people","emoji_order":"715","aliases":[],"aliases_ascii":[],"keywords":[]},"runner_tone5":{"unicode":"1f3c3-1f3ff","unicode_alt":"","code_decimal":"🏃🏿","name":"runner tone 5","shortname":":runner_tone5:","category":"people","emoji_order":"716","aliases":[],"aliases_ascii":[],"keywords":[]},"dancer":{"unicode":"1f483","unicode_alt":"","code_decimal":"💃","name":"dancer","shortname":":dancer:","category":"people","emoji_order":"729","aliases":[],"aliases_ascii":[],"keywords":["people","women","sexy","diversity","girls night","dance"]},"dancer_tone1":{"unicode":"1f483-1f3fb","unicode_alt":"","code_decimal":"💃🏻","name":"dancer tone 1","shortname":":dancer_tone1:","category":"people","emoji_order":"730","aliases":[],"aliases_ascii":[],"keywords":[]},"dancer_tone2":{"unicode":"1f483-1f3fc","unicode_alt":"","code_decimal":"💃🏼","name":"dancer tone 2","shortname":":dancer_tone2:","category":"people","emoji_order":"731","aliases":[],"aliases_ascii":[],"keywords":[]},"dancer_tone3":{"unicode":"1f483-1f3fd","unicode_alt":"","code_decimal":"💃🏽","name":"dancer tone 3","shortname":":dancer_tone3:","category":"people","emoji_order":"732","aliases":[],"aliases_ascii":[],"keywords":[]},"dancer_tone4":{"unicode":"1f483-1f3fe","unicode_alt":"","code_decimal":"💃🏾","name":"dancer tone 4","shortname":":dancer_tone4:","category":"people","emoji_order":"733","aliases":[],"aliases_ascii":[],"keywords":[]},"dancer_tone5":{"unicode":"1f483-1f3ff","unicode_alt":"","code_decimal":"💃🏿","name":"dancer tone 5","shortname":":dancer_tone5:","category":"people","emoji_order":"734","aliases":[],"aliases_ascii":[],"keywords":[]},"man_dancing":{"unicode":"1f57a","unicode_alt":"","code_decimal":"🕺","name":"man dancing","shortname":":man_dancing:","category":"people","emoji_order":"735","aliases":[":male_dancer:"],"aliases_ascii":[],"keywords":[]},"man_dancing_tone1":{"unicode":"1f57a-1f3fb","unicode_alt":"","code_decimal":"🕺🏻","name":"man dancing tone 1","shortname":":man_dancing_tone1:","category":"people","emoji_order":"736","aliases":[":male_dancer_tone1:"],"aliases_ascii":[],"keywords":[]},"man_dancing_tone2":{"unicode":"1f57a-1f3fc","unicode_alt":"","code_decimal":"🕺🏼","name":"man dancing tone 2","shortname":":man_dancing_tone2:","category":"people","emoji_order":"737","aliases":[":male_dancer_tone2:"],"aliases_ascii":[],"keywords":[]},"man_dancing_tone3":{"unicode":"1f57a-1f3fd","unicode_alt":"","code_decimal":"🕺🏽","name":"man dancing tone 3","shortname":":man_dancing_tone3:","category":"people","emoji_order":"738","aliases":[":male_dancer_tone3:"],"aliases_ascii":[],"keywords":[]},"man_dancing_tone4":{"unicode":"1f57a-1f3fe","unicode_alt":"","code_decimal":"🕺🏾","name":"man dancing tone 4","shortname":":man_dancing_tone4:","category":"people","emoji_order":"739","aliases":[":male_dancer_tone4:"],"aliases_ascii":[],"keywords":[]},"man_dancing_tone5":{"unicode":"1f57a-1f3ff","unicode_alt":"","code_decimal":"🕺🏿","name":"man dancing tone 5","shortname":":man_dancing_tone5:","category":"people","emoji_order":"740","aliases":[":male_dancer_tone5:"],"aliases_ascii":[],"keywords":[]},"dancers":{"unicode":"1f46f","unicode_alt":"","code_decimal":"👯","name":"woman with bunny ears","shortname":":dancers:","category":"people","emoji_order":"741","aliases":[],"aliases_ascii":[],"keywords":["people","women","sexy","girls night","boys night","parties","dance"]},"levitate":{"unicode":"1f574","unicode_alt":"1f574-fe0f","code_decimal":"🕴","name":"man in business suit levitating","shortname":":levitate:","category":"activity","emoji_order":"759","aliases":[":man_in_business_suit_levitating:"],"aliases_ascii":[],"keywords":["men","job"]},"speaking_head":{"unicode":"1f5e3","unicode_alt":"1f5e3-fe0f","code_decimal":"🗣","name":"speaking head in silhouette","shortname":":speaking_head:","category":"people","emoji_order":"765","aliases":[":speaking_head_in_silhouette:"],"aliases_ascii":[],"keywords":["people","talk"]},"bust_in_silhouette":{"unicode":"1f464","unicode_alt":"","code_decimal":"👤","name":"bust in silhouette","shortname":":bust_in_silhouette:","category":"people","emoji_order":"766","aliases":[],"aliases_ascii":[],"keywords":["people"]},"busts_in_silhouette":{"unicode":"1f465","unicode_alt":"","code_decimal":"👥","name":"busts in silhouette","shortname":":busts_in_silhouette:","category":"people","emoji_order":"767","aliases":[],"aliases_ascii":[],"keywords":["people"]},"fencer":{"unicode":"1f93a","unicode_alt":"","code_decimal":"🤺","name":"fencer","shortname":":fencer:","category":"activity","emoji_order":"768","aliases":[":fencing:"],"aliases_ascii":[],"keywords":[]},"horse_racing":{"unicode":"1f3c7","unicode_alt":"","code_decimal":"🏇","name":"horse racing","shortname":":horse_racing:","category":"activity","emoji_order":"769","aliases":[],"aliases_ascii":[],"keywords":["men","sport","horse racing"]},"horse_racing_tone1":{"unicode":"1f3c7-1f3fb","unicode_alt":"","code_decimal":"🏇🏻","name":"horse racing tone 1","shortname":":horse_racing_tone1:","category":"activity","emoji_order":"770","aliases":[],"aliases_ascii":[],"keywords":[]},"horse_racing_tone2":{"unicode":"1f3c7-1f3fc","unicode_alt":"","code_decimal":"🏇🏼","name":"horse racing tone 2","shortname":":horse_racing_tone2:","category":"activity","emoji_order":"771","aliases":[],"aliases_ascii":[],"keywords":[]},"horse_racing_tone3":{"unicode":"1f3c7-1f3fd","unicode_alt":"","code_decimal":"🏇🏽","name":"horse racing tone 3","shortname":":horse_racing_tone3:","category":"activity","emoji_order":"772","aliases":[],"aliases_ascii":[],"keywords":[]},"horse_racing_tone4":{"unicode":"1f3c7-1f3fe","unicode_alt":"","code_decimal":"🏇🏾","name":"horse racing tone 4","shortname":":horse_racing_tone4:","category":"activity","emoji_order":"773","aliases":[],"aliases_ascii":[],"keywords":[]},"horse_racing_tone5":{"unicode":"1f3c7-1f3ff","unicode_alt":"","code_decimal":"🏇🏿","name":"horse racing tone 5","shortname":":horse_racing_tone5:","category":"activity","emoji_order":"774","aliases":[],"aliases_ascii":[],"keywords":[]},"skier":{"unicode":"26f7","unicode_alt":"26f7-fe0f","code_decimal":"⛷","name":"skier","shortname":":skier:","category":"activity","emoji_order":"775","aliases":[],"aliases_ascii":[],"keywords":["hat","vacation","cold","sport","skiing"]},"snowboarder":{"unicode":"1f3c2","unicode_alt":"","code_decimal":"🏂","name":"snowboarder","shortname":":snowboarder:","category":"activity","emoji_order":"776","aliases":[],"aliases_ascii":[],"keywords":["hat","vacation","cold","sport","snowboarding"]},"golfer":{"unicode":"1f3cc","unicode_alt":"1f3cc-fe0f","code_decimal":"🏌","name":"golfer","shortname":":golfer:","category":"activity","emoji_order":"782","aliases":[],"aliases_ascii":[],"keywords":["men","game","ball","vacation","sport","golf"]},"surfer":{"unicode":"1f3c4","unicode_alt":"","code_decimal":"🏄","name":"surfer","shortname":":surfer:","category":"activity","emoji_order":"800","aliases":[],"aliases_ascii":[],"keywords":["men","vacation","tropical","sport","diversity"]},"surfer_tone1":{"unicode":"1f3c4-1f3fb","unicode_alt":"","code_decimal":"🏄🏻","name":"surfer tone 1","shortname":":surfer_tone1:","category":"activity","emoji_order":"801","aliases":[],"aliases_ascii":[],"keywords":[]},"surfer_tone2":{"unicode":"1f3c4-1f3fc","unicode_alt":"","code_decimal":"🏄🏼","name":"surfer tone 2","shortname":":surfer_tone2:","category":"activity","emoji_order":"802","aliases":[],"aliases_ascii":[],"keywords":[]},"surfer_tone3":{"unicode":"1f3c4-1f3fd","unicode_alt":"","code_decimal":"🏄🏽","name":"surfer tone 3","shortname":":surfer_tone3:","category":"activity","emoji_order":"803","aliases":[],"aliases_ascii":[],"keywords":[]},"surfer_tone4":{"unicode":"1f3c4-1f3fe","unicode_alt":"","code_decimal":"🏄🏾","name":"surfer tone 4","shortname":":surfer_tone4:","category":"activity","emoji_order":"804","aliases":[],"aliases_ascii":[],"keywords":[]},"surfer_tone5":{"unicode":"1f3c4-1f3ff","unicode_alt":"","code_decimal":"🏄🏿","name":"surfer tone 5","shortname":":surfer_tone5:","category":"activity","emoji_order":"805","aliases":[],"aliases_ascii":[],"keywords":[]},"rowboat":{"unicode":"1f6a3","unicode_alt":"","code_decimal":"🚣","name":"rowboat","shortname":":rowboat:","category":"activity","emoji_order":"818","aliases":[],"aliases_ascii":[],"keywords":["men","workout","sport","rowing","diversity"]},"rowboat_tone1":{"unicode":"1f6a3-1f3fb","unicode_alt":"","code_decimal":"🚣🏻","name":"rowboat tone 1","shortname":":rowboat_tone1:","category":"activity","emoji_order":"819","aliases":[],"aliases_ascii":[],"keywords":[]},"rowboat_tone2":{"unicode":"1f6a3-1f3fc","unicode_alt":"","code_decimal":"🚣🏼","name":"rowboat tone 2","shortname":":rowboat_tone2:","category":"activity","emoji_order":"820","aliases":[],"aliases_ascii":[],"keywords":[]},"rowboat_tone3":{"unicode":"1f6a3-1f3fd","unicode_alt":"","code_decimal":"🚣🏽","name":"rowboat tone 3","shortname":":rowboat_tone3:","category":"activity","emoji_order":"821","aliases":[],"aliases_ascii":[],"keywords":[]},"rowboat_tone4":{"unicode":"1f6a3-1f3fe","unicode_alt":"","code_decimal":"🚣🏾","name":"rowboat tone 4","shortname":":rowboat_tone4:","category":"activity","emoji_order":"822","aliases":[],"aliases_ascii":[],"keywords":[]},"rowboat_tone5":{"unicode":"1f6a3-1f3ff","unicode_alt":"","code_decimal":"🚣🏿","name":"rowboat tone 5","shortname":":rowboat_tone5:","category":"activity","emoji_order":"823","aliases":[],"aliases_ascii":[],"keywords":[]},"swimmer":{"unicode":"1f3ca","unicode_alt":"","code_decimal":"🏊","name":"swimmer","shortname":":swimmer:","category":"activity","emoji_order":"836","aliases":[],"aliases_ascii":[],"keywords":["workout","sport","swim","diversity"]},"swimmer_tone1":{"unicode":"1f3ca-1f3fb","unicode_alt":"","code_decimal":"🏊🏻","name":"swimmer tone 1","shortname":":swimmer_tone1:","category":"activity","emoji_order":"837","aliases":[],"aliases_ascii":[],"keywords":[]},"swimmer_tone2":{"unicode":"1f3ca-1f3fc","unicode_alt":"","code_decimal":"🏊🏼","name":"swimmer tone 2","shortname":":swimmer_tone2:","category":"activity","emoji_order":"838","aliases":[],"aliases_ascii":[],"keywords":[]},"swimmer_tone3":{"unicode":"1f3ca-1f3fd","unicode_alt":"","code_decimal":"🏊🏽","name":"swimmer tone 3","shortname":":swimmer_tone3:","category":"activity","emoji_order":"839","aliases":[],"aliases_ascii":[],"keywords":[]},"swimmer_tone4":{"unicode":"1f3ca-1f3fe","unicode_alt":"","code_decimal":"🏊🏾","name":"swimmer tone 4","shortname":":swimmer_tone4:","category":"activity","emoji_order":"840","aliases":[],"aliases_ascii":[],"keywords":[]},"swimmer_tone5":{"unicode":"1f3ca-1f3ff","unicode_alt":"","code_decimal":"🏊🏿","name":"swimmer tone 5","shortname":":swimmer_tone5:","category":"activity","emoji_order":"841","aliases":[],"aliases_ascii":[],"keywords":[]},"basketball_player":{"unicode":"26f9","unicode_alt":"26f9-fe0f","code_decimal":"⛹","name":"person with ball","shortname":":basketball_player:","category":"activity","emoji_order":"854","aliases":[":person_with_ball:"],"aliases_ascii":[],"keywords":["men","game","ball","sport","basketball","diversity"]},"basketball_player_tone1":{"unicode":"26f9-1f3fb","unicode_alt":"","code_decimal":"⛹🏻","name":"person with ball tone 1","shortname":":basketball_player_tone1:","category":"activity","emoji_order":"855","aliases":[":person_with_ball_tone1:"],"aliases_ascii":[],"keywords":[]},"basketball_player_tone2":{"unicode":"26f9-1f3fc","unicode_alt":"","code_decimal":"⛹🏼","name":"person with ball tone 2","shortname":":basketball_player_tone2:","category":"activity","emoji_order":"856","aliases":[":person_with_ball_tone2:"],"aliases_ascii":[],"keywords":[]},"basketball_player_tone3":{"unicode":"26f9-1f3fd","unicode_alt":"","code_decimal":"⛹🏽","name":"person with ball tone 3","shortname":":basketball_player_tone3:","category":"activity","emoji_order":"857","aliases":[":person_with_ball_tone3:"],"aliases_ascii":[],"keywords":[]},"basketball_player_tone4":{"unicode":"26f9-1f3fe","unicode_alt":"","code_decimal":"⛹🏾","name":"person with ball tone 4","shortname":":basketball_player_tone4:","category":"activity","emoji_order":"858","aliases":[":person_with_ball_tone4:"],"aliases_ascii":[],"keywords":[]},"basketball_player_tone5":{"unicode":"26f9-1f3ff","unicode_alt":"","code_decimal":"⛹🏿","name":"person with ball tone 5","shortname":":basketball_player_tone5:","category":"activity","emoji_order":"859","aliases":[":person_with_ball_tone5:"],"aliases_ascii":[],"keywords":[]},"lifter":{"unicode":"1f3cb","unicode_alt":"1f3cb-fe0f","code_decimal":"🏋","name":"weight lifter","shortname":":lifter:","category":"activity","emoji_order":"872","aliases":[":weight_lifter:"],"aliases_ascii":[],"keywords":["men","workout","flex","sport","weight lifting","win","diversity"]},"lifter_tone1":{"unicode":"1f3cb-1f3fb","unicode_alt":"","code_decimal":"🏋🏻","name":"weight lifter tone 1","shortname":":lifter_tone1:","category":"activity","emoji_order":"873","aliases":[":weight_lifter_tone1:"],"aliases_ascii":[],"keywords":[]},"lifter_tone2":{"unicode":"1f3cb-1f3fc","unicode_alt":"","code_decimal":"🏋🏼","name":"weight lifter tone 2","shortname":":lifter_tone2:","category":"activity","emoji_order":"874","aliases":[":weight_lifter_tone2:"],"aliases_ascii":[],"keywords":[]},"lifter_tone3":{"unicode":"1f3cb-1f3fd","unicode_alt":"","code_decimal":"🏋🏽","name":"weight lifter tone 3","shortname":":lifter_tone3:","category":"activity","emoji_order":"875","aliases":[":weight_lifter_tone3:"],"aliases_ascii":[],"keywords":[]},"lifter_tone4":{"unicode":"1f3cb-1f3fe","unicode_alt":"","code_decimal":"🏋🏾","name":"weight lifter tone 4","shortname":":lifter_tone4:","category":"activity","emoji_order":"876","aliases":[":weight_lifter_tone4:"],"aliases_ascii":[],"keywords":[]},"lifter_tone5":{"unicode":"1f3cb-1f3ff","unicode_alt":"","code_decimal":"🏋🏿","name":"weight lifter tone 5","shortname":":lifter_tone5:","category":"activity","emoji_order":"877","aliases":[":weight_lifter_tone5:"],"aliases_ascii":[],"keywords":[]},"bicyclist":{"unicode":"1f6b4","unicode_alt":"","code_decimal":"🚴","name":"bicyclist","shortname":":bicyclist:","category":"activity","emoji_order":"890","aliases":[],"aliases_ascii":[],"keywords":["men","workout","sport","bike","diversity"]},"bicyclist_tone1":{"unicode":"1f6b4-1f3fb","unicode_alt":"","code_decimal":"🚴🏻","name":"bicyclist tone 1","shortname":":bicyclist_tone1:","category":"activity","emoji_order":"891","aliases":[],"aliases_ascii":[],"keywords":[]},"bicyclist_tone2":{"unicode":"1f6b4-1f3fc","unicode_alt":"","code_decimal":"🚴🏼","name":"bicyclist tone 2","shortname":":bicyclist_tone2:","category":"activity","emoji_order":"892","aliases":[],"aliases_ascii":[],"keywords":[]},"bicyclist_tone3":{"unicode":"1f6b4-1f3fd","unicode_alt":"","code_decimal":"🚴🏽","name":"bicyclist tone 3","shortname":":bicyclist_tone3:","category":"activity","emoji_order":"893","aliases":[],"aliases_ascii":[],"keywords":[]},"bicyclist_tone4":{"unicode":"1f6b4-1f3fe","unicode_alt":"","code_decimal":"🚴🏾","name":"bicyclist tone 4","shortname":":bicyclist_tone4:","category":"activity","emoji_order":"894","aliases":[],"aliases_ascii":[],"keywords":[]},"bicyclist_tone5":{"unicode":"1f6b4-1f3ff","unicode_alt":"","code_decimal":"🚴🏿","name":"bicyclist tone 5","shortname":":bicyclist_tone5:","category":"activity","emoji_order":"895","aliases":[],"aliases_ascii":[],"keywords":[]},"mountain_bicyclist":{"unicode":"1f6b5","unicode_alt":"","code_decimal":"🚵","name":"mountain bicyclist","shortname":":mountain_bicyclist:","category":"activity","emoji_order":"908","aliases":[],"aliases_ascii":[],"keywords":["men","sport","bike","diversity"]},"mountain_bicyclist_tone1":{"unicode":"1f6b5-1f3fb","unicode_alt":"","code_decimal":"🚵🏻","name":"mountain bicyclist tone 1","shortname":":mountain_bicyclist_tone1:","category":"activity","emoji_order":"909","aliases":[],"aliases_ascii":[],"keywords":[]},"mountain_bicyclist_tone2":{"unicode":"1f6b5-1f3fc","unicode_alt":"","code_decimal":"🚵🏼","name":"mountain bicyclist tone 2","shortname":":mountain_bicyclist_tone2:","category":"activity","emoji_order":"910","aliases":[],"aliases_ascii":[],"keywords":[]},"mountain_bicyclist_tone3":{"unicode":"1f6b5-1f3fd","unicode_alt":"","code_decimal":"🚵🏽","name":"mountain bicyclist tone 3","shortname":":mountain_bicyclist_tone3:","category":"activity","emoji_order":"911","aliases":[],"aliases_ascii":[],"keywords":[]},"mountain_bicyclist_tone4":{"unicode":"1f6b5-1f3fe","unicode_alt":"","code_decimal":"🚵🏾","name":"mountain bicyclist tone 4","shortname":":mountain_bicyclist_tone4:","category":"activity","emoji_order":"912","aliases":[],"aliases_ascii":[],"keywords":[]},"mountain_bicyclist_tone5":{"unicode":"1f6b5-1f3ff","unicode_alt":"","code_decimal":"🚵🏿","name":"mountain bicyclist tone 5","shortname":":mountain_bicyclist_tone5:","category":"activity","emoji_order":"913","aliases":[],"aliases_ascii":[],"keywords":[]},"race_car":{"unicode":"1f3ce","unicode_alt":"1f3ce-fe0f","code_decimal":"🏎","name":"racing car","shortname":":race_car:","category":"travel","emoji_order":"926","aliases":[":racing_car:"],"aliases_ascii":[],"keywords":["transportation","car"]},"motorcycle":{"unicode":"1f3cd","unicode_alt":"1f3cd-fe0f","code_decimal":"🏍","name":"racing motorcycle","shortname":":motorcycle:","category":"travel","emoji_order":"927","aliases":[":racing_motorcycle:"],"aliases_ascii":[],"keywords":["transportation","travel","bike"]},"cartwheel":{"unicode":"1f938","unicode_alt":"","code_decimal":"🤸","name":"person doing cartwheel","shortname":":cartwheel:","category":"activity","emoji_order":"928","aliases":[":person_doing_cartwheel:"],"aliases_ascii":[],"keywords":[]},"cartwheel_tone1":{"unicode":"1f938-1f3fb","unicode_alt":"","code_decimal":"🤸🏻","name":"person doing cartwheel tone 1","shortname":":cartwheel_tone1:","category":"activity","emoji_order":"929","aliases":[":person_doing_cartwheel_tone1:"],"aliases_ascii":[],"keywords":[]},"cartwheel_tone2":{"unicode":"1f938-1f3fc","unicode_alt":"","code_decimal":"🤸🏼","name":"person doing cartwheel tone 2","shortname":":cartwheel_tone2:","category":"activity","emoji_order":"930","aliases":[":person_doing_cartwheel_tone2:"],"aliases_ascii":[],"keywords":[]},"cartwheel_tone3":{"unicode":"1f938-1f3fd","unicode_alt":"","code_decimal":"🤸🏽","name":"person doing cartwheel tone 3","shortname":":cartwheel_tone3:","category":"activity","emoji_order":"931","aliases":[":person_doing_cartwheel_tone3:"],"aliases_ascii":[],"keywords":[]},"cartwheel_tone4":{"unicode":"1f938-1f3fe","unicode_alt":"","code_decimal":"🤸🏾","name":"person doing cartwheel tone 4","shortname":":cartwheel_tone4:","category":"activity","emoji_order":"932","aliases":[":person_doing_cartwheel_tone4:"],"aliases_ascii":[],"keywords":[]},"cartwheel_tone5":{"unicode":"1f938-1f3ff","unicode_alt":"","code_decimal":"🤸🏿","name":"person doing cartwheel tone 5","shortname":":cartwheel_tone5:","category":"activity","emoji_order":"933","aliases":[":person_doing_cartwheel_tone5:"],"aliases_ascii":[],"keywords":[]},"wrestlers":{"unicode":"1f93c","unicode_alt":"","code_decimal":"🤼","name":"wrestlers","shortname":":wrestlers:","category":"activity","emoji_order":"946","aliases":[":wrestling:"],"aliases_ascii":[],"keywords":[]},"wrestlers_tone1":{"unicode":"1f93c-1f3fb","unicode_alt":"","code_decimal":"🤼🏻","name":"wrestlers tone 1","shortname":":wrestlers_tone1:","category":"activity","emoji_order":"947","aliases":[":wrestling_tone1:"],"aliases_ascii":[],"keywords":[]},"wrestlers_tone2":{"unicode":"1f93c-1f3fc","unicode_alt":"","code_decimal":"🤼🏼","name":"wrestlers tone 2","shortname":":wrestlers_tone2:","category":"activity","emoji_order":"948","aliases":[":wrestling_tone2:"],"aliases_ascii":[],"keywords":[]},"wrestlers_tone3":{"unicode":"1f93c-1f3fd","unicode_alt":"","code_decimal":"🤼🏽","name":"wrestlers tone 3","shortname":":wrestlers_tone3:","category":"activity","emoji_order":"949","aliases":[":wrestling_tone3:"],"aliases_ascii":[],"keywords":[]},"wrestlers_tone4":{"unicode":"1f93c-1f3fe","unicode_alt":"","code_decimal":"🤼🏾","name":"wrestlers tone 4","shortname":":wrestlers_tone4:","category":"activity","emoji_order":"950","aliases":[":wrestling_tone4:"],"aliases_ascii":[],"keywords":[]},"wrestlers_tone5":{"unicode":"1f93c-1f3ff","unicode_alt":"","code_decimal":"🤼🏿","name":"wrestlers tone 5","shortname":":wrestlers_tone5:","category":"activity","emoji_order":"951","aliases":[":wrestling_tone5:"],"aliases_ascii":[],"keywords":[]},"water_polo":{"unicode":"1f93d","unicode_alt":"","code_decimal":"🤽","name":"water polo","shortname":":water_polo:","category":"activity","emoji_order":"964","aliases":[],"aliases_ascii":[],"keywords":[]},"water_polo_tone1":{"unicode":"1f93d-1f3fb","unicode_alt":"","code_decimal":"🤽🏻","name":"water polo tone 1","shortname":":water_polo_tone1:","category":"activity","emoji_order":"965","aliases":[],"aliases_ascii":[],"keywords":[]},"water_polo_tone2":{"unicode":"1f93d-1f3fc","unicode_alt":"","code_decimal":"🤽🏼","name":"water polo tone 2","shortname":":water_polo_tone2:","category":"activity","emoji_order":"966","aliases":[],"aliases_ascii":[],"keywords":[]},"water_polo_tone3":{"unicode":"1f93d-1f3fd","unicode_alt":"","code_decimal":"🤽🏽","name":"water polo tone 3","shortname":":water_polo_tone3:","category":"activity","emoji_order":"967","aliases":[],"aliases_ascii":[],"keywords":[]},"water_polo_tone4":{"unicode":"1f93d-1f3fe","unicode_alt":"","code_decimal":"🤽🏾","name":"water polo tone 4","shortname":":water_polo_tone4:","category":"activity","emoji_order":"968","aliases":[],"aliases_ascii":[],"keywords":[]},"water_polo_tone5":{"unicode":"1f93d-1f3ff","unicode_alt":"","code_decimal":"🤽🏿","name":"water polo tone 5","shortname":":water_polo_tone5:","category":"activity","emoji_order":"969","aliases":[],"aliases_ascii":[],"keywords":[]},"handball":{"unicode":"1f93e","unicode_alt":"","code_decimal":"🤾","name":"handball","shortname":":handball:","category":"activity","emoji_order":"982","aliases":[],"aliases_ascii":[],"keywords":[]},"handball_tone1":{"unicode":"1f93e-1f3fb","unicode_alt":"","code_decimal":"🤾🏻","name":"handball tone 1","shortname":":handball_tone1:","category":"activity","emoji_order":"983","aliases":[],"aliases_ascii":[],"keywords":[]},"handball_tone2":{"unicode":"1f93e-1f3fc","unicode_alt":"","code_decimal":"🤾🏼","name":"handball tone 2","shortname":":handball_tone2:","category":"activity","emoji_order":"984","aliases":[],"aliases_ascii":[],"keywords":[]},"handball_tone3":{"unicode":"1f93e-1f3fd","unicode_alt":"","code_decimal":"🤾🏽","name":"handball tone 3","shortname":":handball_tone3:","category":"activity","emoji_order":"985","aliases":[],"aliases_ascii":[],"keywords":[]},"handball_tone4":{"unicode":"1f93e-1f3fe","unicode_alt":"","code_decimal":"🤾🏾","name":"handball tone 4","shortname":":handball_tone4:","category":"activity","emoji_order":"986","aliases":[],"aliases_ascii":[],"keywords":[]},"handball_tone5":{"unicode":"1f93e-1f3ff","unicode_alt":"","code_decimal":"🤾🏿","name":"handball tone 5","shortname":":handball_tone5:","category":"activity","emoji_order":"987","aliases":[],"aliases_ascii":[],"keywords":[]},"juggling":{"unicode":"1f939","unicode_alt":"","code_decimal":"🤹","name":"juggling","shortname":":juggling:","category":"activity","emoji_order":"1000","aliases":[":juggler:"],"aliases_ascii":[],"keywords":[]},"juggling_tone1":{"unicode":"1f939-1f3fb","unicode_alt":"","code_decimal":"🤹🏻","name":"juggling tone 1","shortname":":juggling_tone1:","category":"activity","emoji_order":"1001","aliases":[":juggler_tone1:"],"aliases_ascii":[],"keywords":[]},"juggling_tone2":{"unicode":"1f939-1f3fc","unicode_alt":"","code_decimal":"🤹🏼","name":"juggling tone 2","shortname":":juggling_tone2:","category":"activity","emoji_order":"1002","aliases":[":juggler_tone2:"],"aliases_ascii":[],"keywords":[]},"juggling_tone3":{"unicode":"1f939-1f3fd","unicode_alt":"","code_decimal":"🤹🏽","name":"juggling tone 3","shortname":":juggling_tone3:","category":"activity","emoji_order":"1003","aliases":[":juggler_tone3:"],"aliases_ascii":[],"keywords":[]},"juggling_tone4":{"unicode":"1f939-1f3fe","unicode_alt":"","code_decimal":"🤹🏾","name":"juggling tone 4","shortname":":juggling_tone4:","category":"activity","emoji_order":"1004","aliases":[":juggler_tone4:"],"aliases_ascii":[],"keywords":[]},"juggling_tone5":{"unicode":"1f939-1f3ff","unicode_alt":"","code_decimal":"🤹🏿","name":"juggling tone 5","shortname":":juggling_tone5:","category":"activity","emoji_order":"1005","aliases":[":juggler_tone5:"],"aliases_ascii":[],"keywords":[]},"couple":{"unicode":"1f46b","unicode_alt":"","code_decimal":"👫","name":"man and woman holding hands","shortname":":couple:","category":"people","emoji_order":"1018","aliases":[],"aliases_ascii":[],"keywords":["people","sex","creationism"]},"two_men_holding_hands":{"unicode":"1f46c","unicode_alt":"","code_decimal":"👬","name":"two men holding hands","shortname":":two_men_holding_hands:","category":"people","emoji_order":"1024","aliases":[],"aliases_ascii":[],"keywords":["people","gay","men","sex","lgbt"]},"two_women_holding_hands":{"unicode":"1f46d","unicode_alt":"","code_decimal":"👭","name":"two women holding hands","shortname":":two_women_holding_hands:","category":"people","emoji_order":"1030","aliases":[],"aliases_ascii":[],"keywords":["people","women","sex","lgbt","lesbian","girls night"]},"couplekiss":{"unicode":"1f48f","unicode_alt":"","code_decimal":"💏","name":"kiss","shortname":":couplekiss:","category":"people","emoji_order":"1036","aliases":[],"aliases_ascii":[],"keywords":["people","love","sex"]},"kiss_mm":{"unicode":"1f468-2764-1f48b-1f468","unicode_alt":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","code_decimal":"👨❤💋👨","name":"kiss (man,man)","shortname":":kiss_mm:","category":"people","emoji_order":"1038","aliases":[":couplekiss_mm:"],"aliases_ascii":[],"keywords":["people","gay","men","love","sex","lgbt"]},"kiss_ww":{"unicode":"1f469-2764-1f48b-1f469","unicode_alt":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","code_decimal":"👩❤💋👩","name":"kiss (woman,woman)","shortname":":kiss_ww:","category":"people","emoji_order":"1039","aliases":[":couplekiss_ww:"],"aliases_ascii":[],"keywords":["people","women","love","sex","lgbt","lesbian"]},"couple_with_heart":{"unicode":"1f491","unicode_alt":"","code_decimal":"💑","name":"couple with heart","shortname":":couple_with_heart:","category":"people","emoji_order":"1040","aliases":[],"aliases_ascii":[],"keywords":["people","love","sex"]},"couple_mm":{"unicode":"1f468-2764-1f468","unicode_alt":"1f468-200d-2764-fe0f-200d-1f468","code_decimal":"👨❤👨","name":"couple (man,man)","shortname":":couple_mm:","category":"people","emoji_order":"1042","aliases":[":couple_with_heart_mm:"],"aliases_ascii":[],"keywords":["people","gay","men","love","sex","lgbt"]},"couple_ww":{"unicode":"1f469-2764-1f469","unicode_alt":"1f469-200d-2764-fe0f-200d-1f469","code_decimal":"👩❤👩","name":"couple (woman,woman)","shortname":":couple_ww:","category":"people","emoji_order":"1043","aliases":[":couple_with_heart_ww:"],"aliases_ascii":[],"keywords":["people","women","love","sex","lgbt"]},"family":{"unicode":"1f46a","unicode_alt":"","code_decimal":"👪","name":"family","shortname":":family:","category":"people","emoji_order":"1044","aliases":[],"aliases_ascii":[],"keywords":["people","family","baby"]},"family_mwg":{"unicode":"1f468-1f469-1f467","unicode_alt":"1f468-200d-1f469-200d-1f467","code_decimal":"👨👩👧","name":"family (man,woman,girl)","shortname":":family_mwg:","category":"people","emoji_order":"1051","aliases":[],"aliases_ascii":[],"keywords":["people","family","baby"]},"family_mwgb":{"unicode":"1f468-1f469-1f467-1f466","unicode_alt":"1f468-200d-1f469-200d-1f467-200d-1f466","code_decimal":"👨👩👧👦","name":"family (man,woman,girl,boy)","shortname":":family_mwgb:","category":"people","emoji_order":"1052","aliases":[],"aliases_ascii":[],"keywords":["people","family","baby"]},"family_mwbb":{"unicode":"1f468-1f469-1f466-1f466","unicode_alt":"1f468-200d-1f469-200d-1f466-200d-1f466","code_decimal":"👨👩👦👦","name":"family (man,woman,boy,boy)","shortname":":family_mwbb:","category":"people","emoji_order":"1053","aliases":[],"aliases_ascii":[],"keywords":["people","family","baby"]},"family_mwgg":{"unicode":"1f468-1f469-1f467-1f467","unicode_alt":"1f468-200d-1f469-200d-1f467-200d-1f467","code_decimal":"👨👩👧👧","name":"family (man,woman,girl,girl)","shortname":":family_mwgg:","category":"people","emoji_order":"1054","aliases":[],"aliases_ascii":[],"keywords":["people","family","baby"]},"family_mmb":{"unicode":"1f468-1f468-1f466","unicode_alt":"1f468-200d-1f468-200d-1f466","code_decimal":"👨👨👦","name":"family (man,man,boy)","shortname":":family_mmb:","category":"people","emoji_order":"1055","aliases":[],"aliases_ascii":[],"keywords":["people","gay","family","men","baby","lgbt"]},"family_mmg":{"unicode":"1f468-1f468-1f467","unicode_alt":"1f468-200d-1f468-200d-1f467","code_decimal":"👨👨👧","name":"family (man,man,girl)","shortname":":family_mmg:","category":"people","emoji_order":"1056","aliases":[],"aliases_ascii":[],"keywords":["people","gay","family","men","baby","lgbt"]},"family_mmgb":{"unicode":"1f468-1f468-1f467-1f466","unicode_alt":"1f468-200d-1f468-200d-1f467-200d-1f466","code_decimal":"👨👨👧👦","name":"family (man,man,girl,boy)","shortname":":family_mmgb:","category":"people","emoji_order":"1057","aliases":[],"aliases_ascii":[],"keywords":["people","gay","family","men","baby","lgbt"]},"family_mmbb":{"unicode":"1f468-1f468-1f466-1f466","unicode_alt":"1f468-200d-1f468-200d-1f466-200d-1f466","code_decimal":"👨👨👦👦","name":"family (man,man,boy,boy)","shortname":":family_mmbb:","category":"people","emoji_order":"1058","aliases":[],"aliases_ascii":[],"keywords":["people","gay","family","men","baby","lgbt"]},"family_mmgg":{"unicode":"1f468-1f468-1f467-1f467","unicode_alt":"1f468-200d-1f468-200d-1f467-200d-1f467","code_decimal":"👨👨👧👧","name":"family (man,man,girl,girl)","shortname":":family_mmgg:","category":"people","emoji_order":"1059","aliases":[],"aliases_ascii":[],"keywords":["people","gay","family","men","baby","lgbt"]},"family_wwb":{"unicode":"1f469-1f469-1f466","unicode_alt":"1f469-200d-1f469-200d-1f466","code_decimal":"👩👩👦","name":"family (woman,woman,boy)","shortname":":family_wwb:","category":"people","emoji_order":"1060","aliases":[],"aliases_ascii":[],"keywords":["people","family","women","baby","lgbt","lesbian"]},"family_wwg":{"unicode":"1f469-1f469-1f467","unicode_alt":"1f469-200d-1f469-200d-1f467","code_decimal":"👩👩👧","name":"family (woman,woman,girl)","shortname":":family_wwg:","category":"people","emoji_order":"1061","aliases":[],"aliases_ascii":[],"keywords":["people","family","women","baby","lgbt","lesbian"]},"family_wwgb":{"unicode":"1f469-1f469-1f467-1f466","unicode_alt":"1f469-200d-1f469-200d-1f467-200d-1f466","code_decimal":"👩👩👧👦","name":"family (woman,woman,girl,boy)","shortname":":family_wwgb:","category":"people","emoji_order":"1062","aliases":[],"aliases_ascii":[],"keywords":["people","family","women","baby","lgbt","lesbian"]},"family_wwbb":{"unicode":"1f469-1f469-1f466-1f466","unicode_alt":"1f469-200d-1f469-200d-1f466-200d-1f466","code_decimal":"👩👩👦👦","name":"family (woman,woman,boy,boy)","shortname":":family_wwbb:","category":"people","emoji_order":"1063","aliases":[],"aliases_ascii":[],"keywords":["people","family","women","baby","lgbt","lesbian"]},"family_wwgg":{"unicode":"1f469-1f469-1f467-1f467","unicode_alt":"1f469-200d-1f469-200d-1f467-200d-1f467","code_decimal":"👩👩👧👧","name":"family (woman,woman,girl,girl)","shortname":":family_wwgg:","category":"people","emoji_order":"1064","aliases":[],"aliases_ascii":[],"keywords":["people","family","women","baby","lgbt","lesbian"]},"tone1":{"unicode":"1f3fb","unicode_alt":"","code_decimal":"🏻","name":"emoji modifier Fitzpatrick type-1-2","shortname":":tone1:","category":"modifier","emoji_order":"1075","aliases":[],"aliases_ascii":[],"keywords":[]},"tone2":{"unicode":"1f3fc","unicode_alt":"","code_decimal":"🏼","name":"emoji modifier Fitzpatrick type-3","shortname":":tone2:","category":"modifier","emoji_order":"1076","aliases":[],"aliases_ascii":[],"keywords":[]},"tone3":{"unicode":"1f3fd","unicode_alt":"","code_decimal":"🏽","name":"emoji modifier Fitzpatrick type-4","shortname":":tone3:","category":"modifier","emoji_order":"1077","aliases":[],"aliases_ascii":[],"keywords":[]},"tone4":{"unicode":"1f3fe","unicode_alt":"","code_decimal":"🏾","name":"emoji modifier Fitzpatrick type-5","shortname":":tone4:","category":"modifier","emoji_order":"1078","aliases":[],"aliases_ascii":[],"keywords":[]},"tone5":{"unicode":"1f3ff","unicode_alt":"","code_decimal":"🏿","name":"emoji modifier Fitzpatrick type-6","shortname":":tone5:","category":"modifier","emoji_order":"1079","aliases":[],"aliases_ascii":[],"keywords":[]},"muscle":{"unicode":"1f4aa","unicode_alt":"","code_decimal":"💪","name":"flexed biceps","shortname":":muscle:","category":"people","emoji_order":"1080","aliases":[],"aliases_ascii":[],"keywords":["body","hands","workout","flex","win","diversity","feminist","boys night"]},"muscle_tone1":{"unicode":"1f4aa-1f3fb","unicode_alt":"","code_decimal":"💪🏻","name":"flexed biceps tone 1","shortname":":muscle_tone1:","category":"people","emoji_order":"1081","aliases":[],"aliases_ascii":[],"keywords":[]},"muscle_tone2":{"unicode":"1f4aa-1f3fc","unicode_alt":"","code_decimal":"💪🏼","name":"flexed biceps tone 2","shortname":":muscle_tone2:","category":"people","emoji_order":"1082","aliases":[],"aliases_ascii":[],"keywords":[]},"muscle_tone3":{"unicode":"1f4aa-1f3fd","unicode_alt":"","code_decimal":"💪🏽","name":"flexed biceps tone 3","shortname":":muscle_tone3:","category":"people","emoji_order":"1083","aliases":[],"aliases_ascii":[],"keywords":[]},"muscle_tone4":{"unicode":"1f4aa-1f3fe","unicode_alt":"","code_decimal":"💪🏾","name":"flexed biceps tone 4","shortname":":muscle_tone4:","category":"people","emoji_order":"1084","aliases":[],"aliases_ascii":[],"keywords":[]},"muscle_tone5":{"unicode":"1f4aa-1f3ff","unicode_alt":"","code_decimal":"💪🏿","name":"flexed biceps tone 5","shortname":":muscle_tone5:","category":"people","emoji_order":"1085","aliases":[],"aliases_ascii":[],"keywords":[]},"selfie":{"unicode":"1f933","unicode_alt":"","code_decimal":"🤳","name":"selfie","shortname":":selfie:","category":"people","emoji_order":"1086","aliases":[],"aliases_ascii":[],"keywords":[]},"selfie_tone1":{"unicode":"1f933-1f3fb","unicode_alt":"","code_decimal":"🤳🏻","name":"selfie tone 1","shortname":":selfie_tone1:","category":"people","emoji_order":"1087","aliases":[],"aliases_ascii":[],"keywords":[]},"selfie_tone2":{"unicode":"1f933-1f3fc","unicode_alt":"","code_decimal":"🤳🏼","name":"selfie tone 2","shortname":":selfie_tone2:","category":"people","emoji_order":"1088","aliases":[],"aliases_ascii":[],"keywords":[]},"selfie_tone3":{"unicode":"1f933-1f3fd","unicode_alt":"","code_decimal":"🤳🏽","name":"selfie tone 3","shortname":":selfie_tone3:","category":"people","emoji_order":"1089","aliases":[],"aliases_ascii":[],"keywords":[]},"selfie_tone4":{"unicode":"1f933-1f3fe","unicode_alt":"","code_decimal":"🤳🏾","name":"selfie tone 4","shortname":":selfie_tone4:","category":"people","emoji_order":"1090","aliases":[],"aliases_ascii":[],"keywords":[]},"selfie_tone5":{"unicode":"1f933-1f3ff","unicode_alt":"","code_decimal":"🤳🏿","name":"selfie tone 5","shortname":":selfie_tone5:","category":"people","emoji_order":"1091","aliases":[],"aliases_ascii":[],"keywords":[]},"point_left":{"unicode":"1f448","unicode_alt":"","code_decimal":"👈","name":"white left pointing backhand index","shortname":":point_left:","category":"people","emoji_order":"1092","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","diversity"]},"point_left_tone1":{"unicode":"1f448-1f3fb","unicode_alt":"","code_decimal":"👈🏻","name":"white left pointing backhand index tone 1","shortname":":point_left_tone1:","category":"people","emoji_order":"1093","aliases":[],"aliases_ascii":[],"keywords":[]},"point_left_tone2":{"unicode":"1f448-1f3fc","unicode_alt":"","code_decimal":"👈🏼","name":"white left pointing backhand index tone 2","shortname":":point_left_tone2:","category":"people","emoji_order":"1094","aliases":[],"aliases_ascii":[],"keywords":[]},"point_left_tone3":{"unicode":"1f448-1f3fd","unicode_alt":"","code_decimal":"👈🏽","name":"white left pointing backhand index tone 3","shortname":":point_left_tone3:","category":"people","emoji_order":"1095","aliases":[],"aliases_ascii":[],"keywords":[]},"point_left_tone4":{"unicode":"1f448-1f3fe","unicode_alt":"","code_decimal":"👈🏾","name":"white left pointing backhand index tone 4","shortname":":point_left_tone4:","category":"people","emoji_order":"1096","aliases":[],"aliases_ascii":[],"keywords":[]},"point_left_tone5":{"unicode":"1f448-1f3ff","unicode_alt":"","code_decimal":"👈🏿","name":"white left pointing backhand index tone 5","shortname":":point_left_tone5:","category":"people","emoji_order":"1097","aliases":[],"aliases_ascii":[],"keywords":[]},"point_right":{"unicode":"1f449","unicode_alt":"","code_decimal":"👉","name":"white right pointing backhand index","shortname":":point_right:","category":"people","emoji_order":"1098","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","diversity"]},"point_right_tone1":{"unicode":"1f449-1f3fb","unicode_alt":"","code_decimal":"👉🏻","name":"white right pointing backhand index tone 1","shortname":":point_right_tone1:","category":"people","emoji_order":"1099","aliases":[],"aliases_ascii":[],"keywords":[]},"point_right_tone2":{"unicode":"1f449-1f3fc","unicode_alt":"","code_decimal":"👉🏼","name":"white right pointing backhand index tone 2","shortname":":point_right_tone2:","category":"people","emoji_order":"1100","aliases":[],"aliases_ascii":[],"keywords":[]},"point_right_tone3":{"unicode":"1f449-1f3fd","unicode_alt":"","code_decimal":"👉🏽","name":"white right pointing backhand index tone 3","shortname":":point_right_tone3:","category":"people","emoji_order":"1101","aliases":[],"aliases_ascii":[],"keywords":[]},"point_right_tone4":{"unicode":"1f449-1f3fe","unicode_alt":"","code_decimal":"👉🏾","name":"white right pointing backhand index tone 4","shortname":":point_right_tone4:","category":"people","emoji_order":"1102","aliases":[],"aliases_ascii":[],"keywords":[]},"point_right_tone5":{"unicode":"1f449-1f3ff","unicode_alt":"","code_decimal":"👉🏿","name":"white right pointing backhand index tone 5","shortname":":point_right_tone5:","category":"people","emoji_order":"1103","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up":{"unicode":"261d","unicode_alt":"261d-fe0f","code_decimal":"☝","name":"white up pointing index","shortname":":point_up:","category":"people","emoji_order":"1104","aliases":[],"aliases_ascii":[],"keywords":["body","hands","emojione","diversity"]},"point_up_tone1":{"unicode":"261d-1f3fb","unicode_alt":"","code_decimal":"☝🏻","name":"white up pointing index tone 1","shortname":":point_up_tone1:","category":"people","emoji_order":"1105","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_tone2":{"unicode":"261d-1f3fc","unicode_alt":"","code_decimal":"☝🏼","name":"white up pointing index tone 2","shortname":":point_up_tone2:","category":"people","emoji_order":"1106","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_tone3":{"unicode":"261d-1f3fd","unicode_alt":"","code_decimal":"☝🏽","name":"white up pointing index tone 3","shortname":":point_up_tone3:","category":"people","emoji_order":"1107","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_tone4":{"unicode":"261d-1f3fe","unicode_alt":"","code_decimal":"☝🏾","name":"white up pointing index tone 4","shortname":":point_up_tone4:","category":"people","emoji_order":"1108","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_tone5":{"unicode":"261d-1f3ff","unicode_alt":"","code_decimal":"☝🏿","name":"white up pointing index tone 5","shortname":":point_up_tone5:","category":"people","emoji_order":"1109","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_2":{"unicode":"1f446","unicode_alt":"","code_decimal":"👆","name":"white up pointing backhand index","shortname":":point_up_2:","category":"people","emoji_order":"1110","aliases":[],"aliases_ascii":[],"keywords":["body","hands","diversity"]},"point_up_2_tone1":{"unicode":"1f446-1f3fb","unicode_alt":"","code_decimal":"👆🏻","name":"white up pointing backhand index tone 1","shortname":":point_up_2_tone1:","category":"people","emoji_order":"1111","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_2_tone2":{"unicode":"1f446-1f3fc","unicode_alt":"","code_decimal":"👆🏼","name":"white up pointing backhand index tone 2","shortname":":point_up_2_tone2:","category":"people","emoji_order":"1112","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_2_tone3":{"unicode":"1f446-1f3fd","unicode_alt":"","code_decimal":"👆🏽","name":"white up pointing backhand index tone 3","shortname":":point_up_2_tone3:","category":"people","emoji_order":"1113","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_2_tone4":{"unicode":"1f446-1f3fe","unicode_alt":"","code_decimal":"👆🏾","name":"white up pointing backhand index tone 4","shortname":":point_up_2_tone4:","category":"people","emoji_order":"1114","aliases":[],"aliases_ascii":[],"keywords":[]},"point_up_2_tone5":{"unicode":"1f446-1f3ff","unicode_alt":"","code_decimal":"👆🏿","name":"white up pointing backhand index tone 5","shortname":":point_up_2_tone5:","category":"people","emoji_order":"1115","aliases":[],"aliases_ascii":[],"keywords":[]},"middle_finger":{"unicode":"1f595","unicode_alt":"","code_decimal":"🖕","name":"reversed hand with middle finger extended","shortname":":middle_finger:","category":"people","emoji_order":"1116","aliases":[":reversed_hand_with_middle_finger_extended:"],"aliases_ascii":[],"keywords":["body","hands","middle finger","diversity"]},"middle_finger_tone1":{"unicode":"1f595-1f3fb","unicode_alt":"","code_decimal":"🖕🏻","name":"reversed hand with middle finger extended tone 1","shortname":":middle_finger_tone1:","category":"people","emoji_order":"1117","aliases":[":reversed_hand_with_middle_finger_extended_tone1:"],"aliases_ascii":[],"keywords":[]},"middle_finger_tone2":{"unicode":"1f595-1f3fc","unicode_alt":"","code_decimal":"🖕🏼","name":"reversed hand with middle finger extended tone 2","shortname":":middle_finger_tone2:","category":"people","emoji_order":"1118","aliases":[":reversed_hand_with_middle_finger_extended_tone2:"],"aliases_ascii":[],"keywords":[]},"middle_finger_tone3":{"unicode":"1f595-1f3fd","unicode_alt":"","code_decimal":"🖕🏽","name":"reversed hand with middle finger extended tone 3","shortname":":middle_finger_tone3:","category":"people","emoji_order":"1119","aliases":[":reversed_hand_with_middle_finger_extended_tone3:"],"aliases_ascii":[],"keywords":[]},"middle_finger_tone4":{"unicode":"1f595-1f3fe","unicode_alt":"","code_decimal":"🖕🏾","name":"reversed hand with middle finger extended tone 4","shortname":":middle_finger_tone4:","category":"people","emoji_order":"1120","aliases":[":reversed_hand_with_middle_finger_extended_tone4:"],"aliases_ascii":[],"keywords":[]},"middle_finger_tone5":{"unicode":"1f595-1f3ff","unicode_alt":"","code_decimal":"🖕🏿","name":"reversed hand with middle finger extended tone 5","shortname":":middle_finger_tone5:","category":"people","emoji_order":"1121","aliases":[":reversed_hand_with_middle_finger_extended_tone5:"],"aliases_ascii":[],"keywords":[]},"point_down":{"unicode":"1f447","unicode_alt":"","code_decimal":"👇","name":"white down pointing backhand index","shortname":":point_down:","category":"people","emoji_order":"1122","aliases":[],"aliases_ascii":[],"keywords":["body","hands","diversity"]},"point_down_tone1":{"unicode":"1f447-1f3fb","unicode_alt":"","code_decimal":"👇🏻","name":"white down pointing backhand index tone 1","shortname":":point_down_tone1:","category":"people","emoji_order":"1123","aliases":[],"aliases_ascii":[],"keywords":[]},"point_down_tone2":{"unicode":"1f447-1f3fc","unicode_alt":"","code_decimal":"👇🏼","name":"white down pointing backhand index tone 2","shortname":":point_down_tone2:","category":"people","emoji_order":"1124","aliases":[],"aliases_ascii":[],"keywords":[]},"point_down_tone3":{"unicode":"1f447-1f3fd","unicode_alt":"","code_decimal":"👇🏽","name":"white down pointing backhand index tone 3","shortname":":point_down_tone3:","category":"people","emoji_order":"1125","aliases":[],"aliases_ascii":[],"keywords":[]},"point_down_tone4":{"unicode":"1f447-1f3fe","unicode_alt":"","code_decimal":"👇🏾","name":"white down pointing backhand index tone 4","shortname":":point_down_tone4:","category":"people","emoji_order":"1126","aliases":[],"aliases_ascii":[],"keywords":[]},"point_down_tone5":{"unicode":"1f447-1f3ff","unicode_alt":"","code_decimal":"👇🏿","name":"white down pointing backhand index tone 5","shortname":":point_down_tone5:","category":"people","emoji_order":"1127","aliases":[],"aliases_ascii":[],"keywords":[]},"v":{"unicode":"270c","unicode_alt":"270c-fe0f","code_decimal":"✌","name":"victory hand","shortname":":v:","category":"people","emoji_order":"1128","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","thank you","peace","diversity","girls night"]},"v_tone1":{"unicode":"270c-1f3fb","unicode_alt":"","code_decimal":"✌🏻","name":"victory hand tone 1","shortname":":v_tone1:","category":"people","emoji_order":"1129","aliases":[],"aliases_ascii":[],"keywords":[]},"v_tone2":{"unicode":"270c-1f3fc","unicode_alt":"","code_decimal":"✌🏼","name":"victory hand tone 2","shortname":":v_tone2:","category":"people","emoji_order":"1130","aliases":[],"aliases_ascii":[],"keywords":[]},"v_tone3":{"unicode":"270c-1f3fd","unicode_alt":"","code_decimal":"✌🏽","name":"victory hand tone 3","shortname":":v_tone3:","category":"people","emoji_order":"1131","aliases":[],"aliases_ascii":[],"keywords":[]},"v_tone4":{"unicode":"270c-1f3fe","unicode_alt":"","code_decimal":"✌🏾","name":"victory hand tone 4","shortname":":v_tone4:","category":"people","emoji_order":"1132","aliases":[],"aliases_ascii":[],"keywords":[]},"v_tone5":{"unicode":"270c-1f3ff","unicode_alt":"","code_decimal":"✌🏿","name":"victory hand tone 5","shortname":":v_tone5:","category":"people","emoji_order":"1133","aliases":[],"aliases_ascii":[],"keywords":[]},"fingers_crossed":{"unicode":"1f91e","unicode_alt":"","code_decimal":"🤞","name":"hand with first and index finger crossed","shortname":":fingers_crossed:","category":"people","emoji_order":"1134","aliases":[":hand_with_index_and_middle_finger_crossed:"],"aliases_ascii":[],"keywords":[]},"fingers_crossed_tone1":{"unicode":"1f91e-1f3fb","unicode_alt":"","code_decimal":"🤞🏻","name":"hand with index and middle fingers crossed tone 1","shortname":":fingers_crossed_tone1:","category":"people","emoji_order":"1135","aliases":[":hand_with_index_and_middle_fingers_crossed_tone1:"],"aliases_ascii":[],"keywords":[]},"fingers_crossed_tone2":{"unicode":"1f91e-1f3fc","unicode_alt":"","code_decimal":"🤞🏼","name":"hand with index and middle fingers crossed tone 2","shortname":":fingers_crossed_tone2:","category":"people","emoji_order":"1136","aliases":[":hand_with_index_and_middle_fingers_crossed_tone2:"],"aliases_ascii":[],"keywords":[]},"fingers_crossed_tone3":{"unicode":"1f91e-1f3fd","unicode_alt":"","code_decimal":"🤞🏽","name":"hand with index and middle fingers crossed tone 3","shortname":":fingers_crossed_tone3:","category":"people","emoji_order":"1137","aliases":[":hand_with_index_and_middle_fingers_crossed_tone3:"],"aliases_ascii":[],"keywords":[]},"fingers_crossed_tone4":{"unicode":"1f91e-1f3fe","unicode_alt":"","code_decimal":"🤞🏾","name":"hand with index and middle fingers crossed tone 4","shortname":":fingers_crossed_tone4:","category":"people","emoji_order":"1138","aliases":[":hand_with_index_and_middle_fingers_crossed_tone4:"],"aliases_ascii":[],"keywords":[]},"fingers_crossed_tone5":{"unicode":"1f91e-1f3ff","unicode_alt":"","code_decimal":"🤞🏿","name":"hand with index and middle fingers crossed tone 5","shortname":":fingers_crossed_tone5:","category":"people","emoji_order":"1139","aliases":[":hand_with_index_and_middle_fingers_crossed_tone5:"],"aliases_ascii":[],"keywords":[]},"vulcan":{"unicode":"1f596","unicode_alt":"","code_decimal":"🖖","name":"raised hand with part between middle and ring fingers","shortname":":vulcan:","category":"people","emoji_order":"1140","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers:"],"aliases_ascii":[],"keywords":["body","hands","hi","diversity"]},"vulcan_tone1":{"unicode":"1f596-1f3fb","unicode_alt":"","code_decimal":"🖖🏻","name":"raised hand with part between middle and ring fingers tone 1","shortname":":vulcan_tone1:","category":"people","emoji_order":"1141","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone1:"],"aliases_ascii":[],"keywords":[]},"vulcan_tone2":{"unicode":"1f596-1f3fc","unicode_alt":"","code_decimal":"🖖🏼","name":"raised hand with part between middle and ring fingers tone 2","shortname":":vulcan_tone2:","category":"people","emoji_order":"1142","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone2:"],"aliases_ascii":[],"keywords":[]},"vulcan_tone3":{"unicode":"1f596-1f3fd","unicode_alt":"","code_decimal":"🖖🏽","name":"raised hand with part between middle and ring fingers tone 3","shortname":":vulcan_tone3:","category":"people","emoji_order":"1143","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone3:"],"aliases_ascii":[],"keywords":[]},"vulcan_tone4":{"unicode":"1f596-1f3fe","unicode_alt":"","code_decimal":"🖖🏾","name":"raised hand with part between middle and ring fingers tone 4","shortname":":vulcan_tone4:","category":"people","emoji_order":"1144","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone4:"],"aliases_ascii":[],"keywords":[]},"vulcan_tone5":{"unicode":"1f596-1f3ff","unicode_alt":"","code_decimal":"🖖🏿","name":"raised hand with part between middle and ring fingers tone 5","shortname":":vulcan_tone5:","category":"people","emoji_order":"1145","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone5:"],"aliases_ascii":[],"keywords":[]},"metal":{"unicode":"1f918","unicode_alt":"","code_decimal":"🤘","name":"sign of the horns","shortname":":metal:","category":"people","emoji_order":"1146","aliases":[":sign_of_the_horns:"],"aliases_ascii":[],"keywords":["body","hands","hi","diversity","boys night","parties"]},"metal_tone1":{"unicode":"1f918-1f3fb","unicode_alt":"","code_decimal":"🤘🏻","name":"sign of the horns tone 1","shortname":":metal_tone1:","category":"people","emoji_order":"1147","aliases":[":sign_of_the_horns_tone1:"],"aliases_ascii":[],"keywords":[]},"metal_tone2":{"unicode":"1f918-1f3fc","unicode_alt":"","code_decimal":"🤘🏼","name":"sign of the horns tone 2","shortname":":metal_tone2:","category":"people","emoji_order":"1148","aliases":[":sign_of_the_horns_tone2:"],"aliases_ascii":[],"keywords":[]},"metal_tone3":{"unicode":"1f918-1f3fd","unicode_alt":"","code_decimal":"🤘🏽","name":"sign of the horns tone 3","shortname":":metal_tone3:","category":"people","emoji_order":"1149","aliases":[":sign_of_the_horns_tone3:"],"aliases_ascii":[],"keywords":[]},"metal_tone4":{"unicode":"1f918-1f3fe","unicode_alt":"","code_decimal":"🤘🏾","name":"sign of the horns tone 4","shortname":":metal_tone4:","category":"people","emoji_order":"1150","aliases":[":sign_of_the_horns_tone4:"],"aliases_ascii":[],"keywords":[]},"metal_tone5":{"unicode":"1f918-1f3ff","unicode_alt":"","code_decimal":"🤘🏿","name":"sign of the horns tone 5","shortname":":metal_tone5:","category":"people","emoji_order":"1151","aliases":[":sign_of_the_horns_tone5:"],"aliases_ascii":[],"keywords":[]},"call_me":{"unicode":"1f919","unicode_alt":"","code_decimal":"🤙","name":"call me hand","shortname":":call_me:","category":"people","emoji_order":"1152","aliases":[":call_me_hand:"],"aliases_ascii":[],"keywords":[]},"call_me_tone1":{"unicode":"1f919-1f3fb","unicode_alt":"","code_decimal":"🤙🏻","name":"call me hand tone 1","shortname":":call_me_tone1:","category":"people","emoji_order":"1153","aliases":[":call_me_hand_tone1:"],"aliases_ascii":[],"keywords":[]},"call_me_tone2":{"unicode":"1f919-1f3fc","unicode_alt":"","code_decimal":"🤙🏼","name":"call me hand tone 2","shortname":":call_me_tone2:","category":"people","emoji_order":"1154","aliases":[":call_me_hand_tone2:"],"aliases_ascii":[],"keywords":[]},"call_me_tone3":{"unicode":"1f919-1f3fd","unicode_alt":"","code_decimal":"🤙🏽","name":"call me hand tone 3","shortname":":call_me_tone3:","category":"people","emoji_order":"1155","aliases":[":call_me_hand_tone3:"],"aliases_ascii":[],"keywords":[]},"call_me_tone4":{"unicode":"1f919-1f3fe","unicode_alt":"","code_decimal":"🤙🏾","name":"call me hand tone 4","shortname":":call_me_tone4:","category":"people","emoji_order":"1156","aliases":[":call_me_hand_tone4:"],"aliases_ascii":[],"keywords":[]},"call_me_tone5":{"unicode":"1f919-1f3ff","unicode_alt":"","code_decimal":"🤙🏿","name":"call me hand tone 5","shortname":":call_me_tone5:","category":"people","emoji_order":"1157","aliases":[":call_me_hand_tone5:"],"aliases_ascii":[],"keywords":[]},"hand_splayed":{"unicode":"1f590","unicode_alt":"1f590-fe0f","code_decimal":"🖐","name":"raised hand with fingers splayed","shortname":":hand_splayed:","category":"people","emoji_order":"1158","aliases":[":raised_hand_with_fingers_splayed:"],"aliases_ascii":[],"keywords":["body","hands","hi","diversity"]},"hand_splayed_tone1":{"unicode":"1f590-1f3fb","unicode_alt":"","code_decimal":"🖐🏻","name":"raised hand with fingers splayed tone 1","shortname":":hand_splayed_tone1:","category":"people","emoji_order":"1159","aliases":[":raised_hand_with_fingers_splayed_tone1:"],"aliases_ascii":[],"keywords":[]},"hand_splayed_tone2":{"unicode":"1f590-1f3fc","unicode_alt":"","code_decimal":"🖐🏼","name":"raised hand with fingers splayed tone 2","shortname":":hand_splayed_tone2:","category":"people","emoji_order":"1160","aliases":[":raised_hand_with_fingers_splayed_tone2:"],"aliases_ascii":[],"keywords":[]},"hand_splayed_tone3":{"unicode":"1f590-1f3fd","unicode_alt":"","code_decimal":"🖐🏽","name":"raised hand with fingers splayed tone 3","shortname":":hand_splayed_tone3:","category":"people","emoji_order":"1161","aliases":[":raised_hand_with_fingers_splayed_tone3:"],"aliases_ascii":[],"keywords":[]},"hand_splayed_tone4":{"unicode":"1f590-1f3fe","unicode_alt":"","code_decimal":"🖐🏾","name":"raised hand with fingers splayed tone 4","shortname":":hand_splayed_tone4:","category":"people","emoji_order":"1162","aliases":[":raised_hand_with_fingers_splayed_tone4:"],"aliases_ascii":[],"keywords":[]},"hand_splayed_tone5":{"unicode":"1f590-1f3ff","unicode_alt":"","code_decimal":"🖐🏿","name":"raised hand with fingers splayed tone 5","shortname":":hand_splayed_tone5:","category":"people","emoji_order":"1163","aliases":[":raised_hand_with_fingers_splayed_tone5:"],"aliases_ascii":[],"keywords":[]},"raised_hand":{"unicode":"270b","unicode_alt":"","code_decimal":"✋","name":"raised hand","shortname":":raised_hand:","category":"people","emoji_order":"1164","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","diversity","girls night"]},"raised_hand_tone1":{"unicode":"270b-1f3fb","unicode_alt":"","code_decimal":"✋🏻","name":"raised hand tone 1","shortname":":raised_hand_tone1:","category":"people","emoji_order":"1165","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hand_tone2":{"unicode":"270b-1f3fc","unicode_alt":"","code_decimal":"✋🏼","name":"raised hand tone 2","shortname":":raised_hand_tone2:","category":"people","emoji_order":"1166","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hand_tone3":{"unicode":"270b-1f3fd","unicode_alt":"","code_decimal":"✋🏽","name":"raised hand tone 3","shortname":":raised_hand_tone3:","category":"people","emoji_order":"1167","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hand_tone4":{"unicode":"270b-1f3fe","unicode_alt":"","code_decimal":"✋🏾","name":"raised hand tone 4","shortname":":raised_hand_tone4:","category":"people","emoji_order":"1168","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hand_tone5":{"unicode":"270b-1f3ff","unicode_alt":"","code_decimal":"✋🏿","name":"raised hand tone 5","shortname":":raised_hand_tone5:","category":"people","emoji_order":"1169","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_hand":{"unicode":"1f44c","unicode_alt":"","code_decimal":"👌","name":"ok hand sign","shortname":":ok_hand:","category":"people","emoji_order":"1170","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","diversity","perfect","good","beautiful"]},"ok_hand_tone1":{"unicode":"1f44c-1f3fb","unicode_alt":"","code_decimal":"👌🏻","name":"ok hand sign tone 1","shortname":":ok_hand_tone1:","category":"people","emoji_order":"1171","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_hand_tone2":{"unicode":"1f44c-1f3fc","unicode_alt":"","code_decimal":"👌🏼","name":"ok hand sign tone 2","shortname":":ok_hand_tone2:","category":"people","emoji_order":"1172","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_hand_tone3":{"unicode":"1f44c-1f3fd","unicode_alt":"","code_decimal":"👌🏽","name":"ok hand sign tone 3","shortname":":ok_hand_tone3:","category":"people","emoji_order":"1173","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_hand_tone4":{"unicode":"1f44c-1f3fe","unicode_alt":"","code_decimal":"👌🏾","name":"ok hand sign tone 4","shortname":":ok_hand_tone4:","category":"people","emoji_order":"1174","aliases":[],"aliases_ascii":[],"keywords":[]},"ok_hand_tone5":{"unicode":"1f44c-1f3ff","unicode_alt":"","code_decimal":"👌🏿","name":"ok hand sign tone 5","shortname":":ok_hand_tone5:","category":"people","emoji_order":"1175","aliases":[],"aliases_ascii":[],"keywords":[]},"thumbsup":{"unicode":"1f44d","unicode_alt":"","code_decimal":"👍","name":"thumbs up sign","shortname":":thumbsup:","category":"people","emoji_order":"1176","aliases":[":+1:",":thumbup:"],"aliases_ascii":[],"keywords":["body","hands","hi","luck","thank you","diversity","perfect","good","beautiful"]},"thumbsup_tone1":{"unicode":"1f44d-1f3fb","unicode_alt":"","code_decimal":"👍🏻","name":"thumbs up sign tone 1","shortname":":thumbsup_tone1:","category":"people","emoji_order":"1177","aliases":[":+1_tone1:",":thumbup_tone1:"],"aliases_ascii":[],"keywords":[]},"thumbsup_tone2":{"unicode":"1f44d-1f3fc","unicode_alt":"","code_decimal":"👍🏼","name":"thumbs up sign tone 2","shortname":":thumbsup_tone2:","category":"people","emoji_order":"1178","aliases":[":+1_tone2:",":thumbup_tone2:"],"aliases_ascii":[],"keywords":[]},"thumbsup_tone3":{"unicode":"1f44d-1f3fd","unicode_alt":"","code_decimal":"👍🏽","name":"thumbs up sign tone 3","shortname":":thumbsup_tone3:","category":"people","emoji_order":"1179","aliases":[":+1_tone3:",":thumbup_tone3:"],"aliases_ascii":[],"keywords":[]},"thumbsup_tone4":{"unicode":"1f44d-1f3fe","unicode_alt":"","code_decimal":"👍🏾","name":"thumbs up sign tone 4","shortname":":thumbsup_tone4:","category":"people","emoji_order":"1180","aliases":[":+1_tone4:",":thumbup_tone4:"],"aliases_ascii":[],"keywords":[]},"thumbsup_tone5":{"unicode":"1f44d-1f3ff","unicode_alt":"","code_decimal":"👍🏿","name":"thumbs up sign tone 5","shortname":":thumbsup_tone5:","category":"people","emoji_order":"1181","aliases":[":+1_tone5:",":thumbup_tone5:"],"aliases_ascii":[],"keywords":[]},"thumbsdown":{"unicode":"1f44e","unicode_alt":"","code_decimal":"👎","name":"thumbs down sign","shortname":":thumbsdown:","category":"people","emoji_order":"1182","aliases":[":-1:",":thumbdown:"],"aliases_ascii":[],"keywords":["body","hands","diversity"]},"thumbsdown_tone1":{"unicode":"1f44e-1f3fb","unicode_alt":"","code_decimal":"👎🏻","name":"thumbs down sign tone 1","shortname":":thumbsdown_tone1:","category":"people","emoji_order":"1183","aliases":[":-1_tone1:",":thumbdown_tone1:"],"aliases_ascii":[],"keywords":[]},"thumbsdown_tone2":{"unicode":"1f44e-1f3fc","unicode_alt":"","code_decimal":"👎🏼","name":"thumbs down sign tone 2","shortname":":thumbsdown_tone2:","category":"people","emoji_order":"1184","aliases":[":-1_tone2:",":thumbdown_tone2:"],"aliases_ascii":[],"keywords":[]},"thumbsdown_tone3":{"unicode":"1f44e-1f3fd","unicode_alt":"","code_decimal":"👎🏽","name":"thumbs down sign tone 3","shortname":":thumbsdown_tone3:","category":"people","emoji_order":"1185","aliases":[":-1_tone3:",":thumbdown_tone3:"],"aliases_ascii":[],"keywords":[]},"thumbsdown_tone4":{"unicode":"1f44e-1f3fe","unicode_alt":"","code_decimal":"👎🏾","name":"thumbs down sign tone 4","shortname":":thumbsdown_tone4:","category":"people","emoji_order":"1186","aliases":[":-1_tone4:",":thumbdown_tone4:"],"aliases_ascii":[],"keywords":[]},"thumbsdown_tone5":{"unicode":"1f44e-1f3ff","unicode_alt":"","code_decimal":"👎🏿","name":"thumbs down sign tone 5","shortname":":thumbsdown_tone5:","category":"people","emoji_order":"1187","aliases":[":-1_tone5:",":thumbdown_tone5:"],"aliases_ascii":[],"keywords":[]},"fist":{"unicode":"270a","unicode_alt":"","code_decimal":"✊","name":"raised fist","shortname":":fist:","category":"people","emoji_order":"1188","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","fist bump","diversity","condolence"]},"fist_tone1":{"unicode":"270a-1f3fb","unicode_alt":"","code_decimal":"✊🏻","name":"raised fist tone 1","shortname":":fist_tone1:","category":"people","emoji_order":"1189","aliases":[],"aliases_ascii":[],"keywords":[]},"fist_tone2":{"unicode":"270a-1f3fc","unicode_alt":"","code_decimal":"✊🏼","name":"raised fist tone 2","shortname":":fist_tone2:","category":"people","emoji_order":"1190","aliases":[],"aliases_ascii":[],"keywords":[]},"fist_tone3":{"unicode":"270a-1f3fd","unicode_alt":"","code_decimal":"✊🏽","name":"raised fist tone 3","shortname":":fist_tone3:","category":"people","emoji_order":"1191","aliases":[],"aliases_ascii":[],"keywords":[]},"fist_tone4":{"unicode":"270a-1f3fe","unicode_alt":"","code_decimal":"✊🏾","name":"raised fist tone 4","shortname":":fist_tone4:","category":"people","emoji_order":"1192","aliases":[],"aliases_ascii":[],"keywords":[]},"fist_tone5":{"unicode":"270a-1f3ff","unicode_alt":"","code_decimal":"✊🏿","name":"raised fist tone 5","shortname":":fist_tone5:","category":"people","emoji_order":"1193","aliases":[],"aliases_ascii":[],"keywords":[]},"punch":{"unicode":"1f44a","unicode_alt":"","code_decimal":"👊","name":"fisted hand sign","shortname":":punch:","category":"people","emoji_order":"1194","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","fist bump","diversity","boys night"]},"punch_tone1":{"unicode":"1f44a-1f3fb","unicode_alt":"","code_decimal":"👊🏻","name":"fisted hand sign tone 1","shortname":":punch_tone1:","category":"people","emoji_order":"1195","aliases":[],"aliases_ascii":[],"keywords":[]},"punch_tone2":{"unicode":"1f44a-1f3fc","unicode_alt":"","code_decimal":"👊🏼","name":"fisted hand sign tone 2","shortname":":punch_tone2:","category":"people","emoji_order":"1196","aliases":[],"aliases_ascii":[],"keywords":[]},"punch_tone3":{"unicode":"1f44a-1f3fd","unicode_alt":"","code_decimal":"👊🏽","name":"fisted hand sign tone 3","shortname":":punch_tone3:","category":"people","emoji_order":"1197","aliases":[],"aliases_ascii":[],"keywords":[]},"punch_tone4":{"unicode":"1f44a-1f3fe","unicode_alt":"","code_decimal":"👊🏾","name":"fisted hand sign tone 4","shortname":":punch_tone4:","category":"people","emoji_order":"1198","aliases":[],"aliases_ascii":[],"keywords":[]},"punch_tone5":{"unicode":"1f44a-1f3ff","unicode_alt":"","code_decimal":"👊🏿","name":"fisted hand sign tone 5","shortname":":punch_tone5:","category":"people","emoji_order":"1199","aliases":[],"aliases_ascii":[],"keywords":[]},"left_facing_fist":{"unicode":"1f91b","unicode_alt":"","code_decimal":"🤛","name":"left-facing fist","shortname":":left_facing_fist:","category":"people","emoji_order":"1200","aliases":[":left_fist:"],"aliases_ascii":[],"keywords":[]},"left_facing_fist_tone1":{"unicode":"1f91b-1f3fb","unicode_alt":"","code_decimal":"🤛🏻","name":"left facing fist tone 1","shortname":":left_facing_fist_tone1:","category":"people","emoji_order":"1201","aliases":[":left_fist_tone1:"],"aliases_ascii":[],"keywords":[]},"left_facing_fist_tone2":{"unicode":"1f91b-1f3fc","unicode_alt":"","code_decimal":"🤛🏼","name":"left facing fist tone 2","shortname":":left_facing_fist_tone2:","category":"people","emoji_order":"1202","aliases":[":left_fist_tone2:"],"aliases_ascii":[],"keywords":[]},"left_facing_fist_tone3":{"unicode":"1f91b-1f3fd","unicode_alt":"","code_decimal":"🤛🏽","name":"left facing fist tone 3","shortname":":left_facing_fist_tone3:","category":"people","emoji_order":"1203","aliases":[":left_fist_tone3:"],"aliases_ascii":[],"keywords":[]},"left_facing_fist_tone4":{"unicode":"1f91b-1f3fe","unicode_alt":"","code_decimal":"🤛🏾","name":"left facing fist tone 4","shortname":":left_facing_fist_tone4:","category":"people","emoji_order":"1204","aliases":[":left_fist_tone4:"],"aliases_ascii":[],"keywords":[]},"left_facing_fist_tone5":{"unicode":"1f91b-1f3ff","unicode_alt":"","code_decimal":"🤛🏿","name":"left facing fist tone 5","shortname":":left_facing_fist_tone5:","category":"people","emoji_order":"1205","aliases":[":left_fist_tone5:"],"aliases_ascii":[],"keywords":[]},"right_facing_fist":{"unicode":"1f91c","unicode_alt":"","code_decimal":"🤜","name":"right-facing fist","shortname":":right_facing_fist:","category":"people","emoji_order":"1206","aliases":[":right_fist:"],"aliases_ascii":[],"keywords":[]},"right_facing_fist_tone1":{"unicode":"1f91c-1f3fb","unicode_alt":"","code_decimal":"🤜🏻","name":"right facing fist tone 1","shortname":":right_facing_fist_tone1:","category":"people","emoji_order":"1207","aliases":[":right_fist_tone1:"],"aliases_ascii":[],"keywords":[]},"right_facing_fist_tone2":{"unicode":"1f91c-1f3fc","unicode_alt":"","code_decimal":"🤜🏼","name":"right facing fist tone 2","shortname":":right_facing_fist_tone2:","category":"people","emoji_order":"1208","aliases":[":right_fist_tone2:"],"aliases_ascii":[],"keywords":[]},"right_facing_fist_tone3":{"unicode":"1f91c-1f3fd","unicode_alt":"","code_decimal":"🤜🏽","name":"right facing fist tone 3","shortname":":right_facing_fist_tone3:","category":"people","emoji_order":"1209","aliases":[":right_fist_tone3:"],"aliases_ascii":[],"keywords":[]},"right_facing_fist_tone4":{"unicode":"1f91c-1f3fe","unicode_alt":"","code_decimal":"🤜🏾","name":"right facing fist tone 4","shortname":":right_facing_fist_tone4:","category":"people","emoji_order":"1210","aliases":[":right_fist_tone4:"],"aliases_ascii":[],"keywords":[]},"right_facing_fist_tone5":{"unicode":"1f91c-1f3ff","unicode_alt":"","code_decimal":"🤜🏿","name":"right facing fist tone 5","shortname":":right_facing_fist_tone5:","category":"people","emoji_order":"1211","aliases":[":right_fist_tone5:"],"aliases_ascii":[],"keywords":[]},"raised_back_of_hand":{"unicode":"1f91a","unicode_alt":"","code_decimal":"🤚","name":"raised back of hand","shortname":":raised_back_of_hand:","category":"people","emoji_order":"1212","aliases":[":back_of_hand:"],"aliases_ascii":[],"keywords":[]},"raised_back_of_hand_tone1":{"unicode":"1f91a-1f3fb","unicode_alt":"","code_decimal":"🤚🏻","name":"raised back of hand tone 1","shortname":":raised_back_of_hand_tone1:","category":"people","emoji_order":"1213","aliases":[":back_of_hand_tone1:"],"aliases_ascii":[],"keywords":[]},"raised_back_of_hand_tone2":{"unicode":"1f91a-1f3fc","unicode_alt":"","code_decimal":"🤚🏼","name":"raised back of hand tone 2","shortname":":raised_back_of_hand_tone2:","category":"people","emoji_order":"1214","aliases":[":back_of_hand_tone2:"],"aliases_ascii":[],"keywords":[]},"raised_back_of_hand_tone3":{"unicode":"1f91a-1f3fd","unicode_alt":"","code_decimal":"🤚🏽","name":"raised back of hand tone 3","shortname":":raised_back_of_hand_tone3:","category":"people","emoji_order":"1215","aliases":[":back_of_hand_tone3:"],"aliases_ascii":[],"keywords":[]},"raised_back_of_hand_tone4":{"unicode":"1f91a-1f3fe","unicode_alt":"","code_decimal":"🤚🏾","name":"raised back of hand tone 4","shortname":":raised_back_of_hand_tone4:","category":"people","emoji_order":"1216","aliases":[":back_of_hand_tone4:"],"aliases_ascii":[],"keywords":[]},"raised_back_of_hand_tone5":{"unicode":"1f91a-1f3ff","unicode_alt":"","code_decimal":"🤚🏿","name":"raised back of hand tone 5","shortname":":raised_back_of_hand_tone5:","category":"people","emoji_order":"1217","aliases":[":back_of_hand_tone5:"],"aliases_ascii":[],"keywords":[]},"wave":{"unicode":"1f44b","unicode_alt":"","code_decimal":"👋","name":"waving hand sign","shortname":":wave:","category":"people","emoji_order":"1218","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","diversity"]},"wave_tone1":{"unicode":"1f44b-1f3fb","unicode_alt":"","code_decimal":"👋🏻","name":"waving hand sign tone 1","shortname":":wave_tone1:","category":"people","emoji_order":"1219","aliases":[],"aliases_ascii":[],"keywords":[]},"wave_tone2":{"unicode":"1f44b-1f3fc","unicode_alt":"","code_decimal":"👋🏼","name":"waving hand sign tone 2","shortname":":wave_tone2:","category":"people","emoji_order":"1220","aliases":[],"aliases_ascii":[],"keywords":[]},"wave_tone3":{"unicode":"1f44b-1f3fd","unicode_alt":"","code_decimal":"👋🏽","name":"waving hand sign tone 3","shortname":":wave_tone3:","category":"people","emoji_order":"1221","aliases":[],"aliases_ascii":[],"keywords":[]},"wave_tone4":{"unicode":"1f44b-1f3fe","unicode_alt":"","code_decimal":"👋🏾","name":"waving hand sign tone 4","shortname":":wave_tone4:","category":"people","emoji_order":"1222","aliases":[],"aliases_ascii":[],"keywords":[]},"wave_tone5":{"unicode":"1f44b-1f3ff","unicode_alt":"","code_decimal":"👋🏿","name":"waving hand sign tone 5","shortname":":wave_tone5:","category":"people","emoji_order":"1223","aliases":[],"aliases_ascii":[],"keywords":[]},"clap":{"unicode":"1f44f","unicode_alt":"","code_decimal":"👏","name":"clapping hands sign","shortname":":clap:","category":"people","emoji_order":"1224","aliases":[],"aliases_ascii":[],"keywords":["body","hands","win","diversity","good","beautiful"]},"clap_tone1":{"unicode":"1f44f-1f3fb","unicode_alt":"","code_decimal":"👏🏻","name":"clapping hands sign tone 1","shortname":":clap_tone1:","category":"people","emoji_order":"1225","aliases":[],"aliases_ascii":[],"keywords":[]},"clap_tone2":{"unicode":"1f44f-1f3fc","unicode_alt":"","code_decimal":"👏🏼","name":"clapping hands sign tone 2","shortname":":clap_tone2:","category":"people","emoji_order":"1226","aliases":[],"aliases_ascii":[],"keywords":[]},"clap_tone3":{"unicode":"1f44f-1f3fd","unicode_alt":"","code_decimal":"👏🏽","name":"clapping hands sign tone 3","shortname":":clap_tone3:","category":"people","emoji_order":"1227","aliases":[],"aliases_ascii":[],"keywords":[]},"clap_tone4":{"unicode":"1f44f-1f3fe","unicode_alt":"","code_decimal":"👏🏾","name":"clapping hands sign tone 4","shortname":":clap_tone4:","category":"people","emoji_order":"1228","aliases":[],"aliases_ascii":[],"keywords":[]},"clap_tone5":{"unicode":"1f44f-1f3ff","unicode_alt":"","code_decimal":"👏🏿","name":"clapping hands sign tone 5","shortname":":clap_tone5:","category":"people","emoji_order":"1229","aliases":[],"aliases_ascii":[],"keywords":[]},"writing_hand":{"unicode":"270d","unicode_alt":"270d-fe0f","code_decimal":"✍","name":"writing hand","shortname":":writing_hand:","category":"people","emoji_order":"1230","aliases":[],"aliases_ascii":[],"keywords":["body","hands","write","diversity"]},"writing_hand_tone1":{"unicode":"270d-1f3fb","unicode_alt":"","code_decimal":"✍🏻","name":"writing hand tone 1","shortname":":writing_hand_tone1:","category":"people","emoji_order":"1231","aliases":[],"aliases_ascii":[],"keywords":[]},"writing_hand_tone2":{"unicode":"270d-1f3fc","unicode_alt":"","code_decimal":"✍🏼","name":"writing hand tone 2","shortname":":writing_hand_tone2:","category":"people","emoji_order":"1232","aliases":[],"aliases_ascii":[],"keywords":[]},"writing_hand_tone3":{"unicode":"270d-1f3fd","unicode_alt":"","code_decimal":"✍🏽","name":"writing hand tone 3","shortname":":writing_hand_tone3:","category":"people","emoji_order":"1233","aliases":[],"aliases_ascii":[],"keywords":[]},"writing_hand_tone4":{"unicode":"270d-1f3fe","unicode_alt":"","code_decimal":"✍🏾","name":"writing hand tone 4","shortname":":writing_hand_tone4:","category":"people","emoji_order":"1234","aliases":[],"aliases_ascii":[],"keywords":[]},"writing_hand_tone5":{"unicode":"270d-1f3ff","unicode_alt":"","code_decimal":"✍🏿","name":"writing hand tone 5","shortname":":writing_hand_tone5:","category":"people","emoji_order":"1235","aliases":[],"aliases_ascii":[],"keywords":[]},"open_hands":{"unicode":"1f450","unicode_alt":"","code_decimal":"👐","name":"open hands sign","shortname":":open_hands:","category":"people","emoji_order":"1236","aliases":[],"aliases_ascii":[],"keywords":["body","hands","diversity","condolence"]},"open_hands_tone1":{"unicode":"1f450-1f3fb","unicode_alt":"","code_decimal":"👐🏻","name":"open hands sign tone 1","shortname":":open_hands_tone1:","category":"people","emoji_order":"1237","aliases":[],"aliases_ascii":[],"keywords":[]},"open_hands_tone2":{"unicode":"1f450-1f3fc","unicode_alt":"","code_decimal":"👐🏼","name":"open hands sign tone 2","shortname":":open_hands_tone2:","category":"people","emoji_order":"1238","aliases":[],"aliases_ascii":[],"keywords":[]},"open_hands_tone3":{"unicode":"1f450-1f3fd","unicode_alt":"","code_decimal":"👐🏽","name":"open hands sign tone 3","shortname":":open_hands_tone3:","category":"people","emoji_order":"1239","aliases":[],"aliases_ascii":[],"keywords":[]},"open_hands_tone4":{"unicode":"1f450-1f3fe","unicode_alt":"","code_decimal":"👐🏾","name":"open hands sign tone 4","shortname":":open_hands_tone4:","category":"people","emoji_order":"1240","aliases":[],"aliases_ascii":[],"keywords":[]},"open_hands_tone5":{"unicode":"1f450-1f3ff","unicode_alt":"","code_decimal":"👐🏿","name":"open hands sign tone 5","shortname":":open_hands_tone5:","category":"people","emoji_order":"1241","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hands":{"unicode":"1f64c","unicode_alt":"","code_decimal":"🙌","name":"person raising both hands in celebration","shortname":":raised_hands:","category":"people","emoji_order":"1242","aliases":[],"aliases_ascii":[],"keywords":["body","hands","diversity","perfect","good","parties"]},"raised_hands_tone1":{"unicode":"1f64c-1f3fb","unicode_alt":"","code_decimal":"🙌🏻","name":"person raising both hands in celebration tone 1","shortname":":raised_hands_tone1:","category":"people","emoji_order":"1243","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hands_tone2":{"unicode":"1f64c-1f3fc","unicode_alt":"","code_decimal":"🙌🏼","name":"person raising both hands in celebration tone 2","shortname":":raised_hands_tone2:","category":"people","emoji_order":"1244","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hands_tone3":{"unicode":"1f64c-1f3fd","unicode_alt":"","code_decimal":"🙌🏽","name":"person raising both hands in celebration tone 3","shortname":":raised_hands_tone3:","category":"people","emoji_order":"1245","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hands_tone4":{"unicode":"1f64c-1f3fe","unicode_alt":"","code_decimal":"🙌🏾","name":"person raising both hands in celebration tone 4","shortname":":raised_hands_tone4:","category":"people","emoji_order":"1246","aliases":[],"aliases_ascii":[],"keywords":[]},"raised_hands_tone5":{"unicode":"1f64c-1f3ff","unicode_alt":"","code_decimal":"🙌🏿","name":"person raising both hands in celebration tone 5","shortname":":raised_hands_tone5:","category":"people","emoji_order":"1247","aliases":[],"aliases_ascii":[],"keywords":[]},"pray":{"unicode":"1f64f","unicode_alt":"","code_decimal":"🙏","name":"person with folded hands","shortname":":pray:","category":"people","emoji_order":"1248","aliases":[],"aliases_ascii":[],"keywords":["body","hands","hi","luck","thank you","pray","diversity","scientology"]},"pray_tone1":{"unicode":"1f64f-1f3fb","unicode_alt":"","code_decimal":"🙏🏻","name":"person with folded hands tone 1","shortname":":pray_tone1:","category":"people","emoji_order":"1249","aliases":[],"aliases_ascii":[],"keywords":[]},"pray_tone2":{"unicode":"1f64f-1f3fc","unicode_alt":"","code_decimal":"🙏🏼","name":"person with folded hands tone 2","shortname":":pray_tone2:","category":"people","emoji_order":"1250","aliases":[],"aliases_ascii":[],"keywords":[]},"pray_tone3":{"unicode":"1f64f-1f3fd","unicode_alt":"","code_decimal":"🙏🏽","name":"person with folded hands tone 3","shortname":":pray_tone3:","category":"people","emoji_order":"1251","aliases":[],"aliases_ascii":[],"keywords":[]},"pray_tone4":{"unicode":"1f64f-1f3fe","unicode_alt":"","code_decimal":"🙏🏾","name":"person with folded hands tone 4","shortname":":pray_tone4:","category":"people","emoji_order":"1252","aliases":[],"aliases_ascii":[],"keywords":[]},"pray_tone5":{"unicode":"1f64f-1f3ff","unicode_alt":"","code_decimal":"🙏🏿","name":"person with folded hands tone 5","shortname":":pray_tone5:","category":"people","emoji_order":"1253","aliases":[],"aliases_ascii":[],"keywords":[]},"handshake":{"unicode":"1f91d","unicode_alt":"","code_decimal":"🤝","name":"handshake","shortname":":handshake:","category":"people","emoji_order":"1254","aliases":[":shaking_hands:"],"aliases_ascii":[],"keywords":[]},"handshake_tone1":{"unicode":"1f91d-1f3fb","unicode_alt":"","code_decimal":"🤝🏻","name":"handshake tone 1","shortname":":handshake_tone1:","category":"people","emoji_order":"1255","aliases":[":shaking_hands_tone1:"],"aliases_ascii":[],"keywords":[]},"handshake_tone2":{"unicode":"1f91d-1f3fc","unicode_alt":"","code_decimal":"🤝🏼","name":"handshake tone 2","shortname":":handshake_tone2:","category":"people","emoji_order":"1256","aliases":[":shaking_hands_tone2:"],"aliases_ascii":[],"keywords":[]},"handshake_tone3":{"unicode":"1f91d-1f3fd","unicode_alt":"","code_decimal":"🤝🏽","name":"handshake tone 3","shortname":":handshake_tone3:","category":"people","emoji_order":"1257","aliases":[":shaking_hands_tone3:"],"aliases_ascii":[],"keywords":[]},"handshake_tone4":{"unicode":"1f91d-1f3fe","unicode_alt":"","code_decimal":"🤝🏾","name":"handshake tone 4","shortname":":handshake_tone4:","category":"people","emoji_order":"1258","aliases":[":shaking_hands_tone4:"],"aliases_ascii":[],"keywords":[]},"handshake_tone5":{"unicode":"1f91d-1f3ff","unicode_alt":"","code_decimal":"🤝🏿","name":"handshake tone 5","shortname":":handshake_tone5:","category":"people","emoji_order":"1259","aliases":[":shaking_hands_tone5:"],"aliases_ascii":[],"keywords":[]},"nail_care":{"unicode":"1f485","unicode_alt":"","code_decimal":"💅","name":"nail polish","shortname":":nail_care:","category":"people","emoji_order":"1260","aliases":[],"aliases_ascii":[],"keywords":["women","body","hands","nailpolish","diversity","girls night"]},"nail_care_tone1":{"unicode":"1f485-1f3fb","unicode_alt":"","code_decimal":"💅🏻","name":"nail polish tone 1","shortname":":nail_care_tone1:","category":"people","emoji_order":"1261","aliases":[],"aliases_ascii":[],"keywords":[]},"nail_care_tone2":{"unicode":"1f485-1f3fc","unicode_alt":"","code_decimal":"💅🏼","name":"nail polish tone 2","shortname":":nail_care_tone2:","category":"people","emoji_order":"1262","aliases":[],"aliases_ascii":[],"keywords":[]},"nail_care_tone3":{"unicode":"1f485-1f3fd","unicode_alt":"","code_decimal":"💅🏽","name":"nail polish tone 3","shortname":":nail_care_tone3:","category":"people","emoji_order":"1263","aliases":[],"aliases_ascii":[],"keywords":[]},"nail_care_tone4":{"unicode":"1f485-1f3fe","unicode_alt":"","code_decimal":"💅🏾","name":"nail polish tone 4","shortname":":nail_care_tone4:","category":"people","emoji_order":"1264","aliases":[],"aliases_ascii":[],"keywords":[]},"nail_care_tone5":{"unicode":"1f485-1f3ff","unicode_alt":"","code_decimal":"💅🏿","name":"nail polish tone 5","shortname":":nail_care_tone5:","category":"people","emoji_order":"1265","aliases":[],"aliases_ascii":[],"keywords":[]},"ear":{"unicode":"1f442","unicode_alt":"","code_decimal":"👂","name":"ear","shortname":":ear:","category":"people","emoji_order":"1266","aliases":[],"aliases_ascii":[],"keywords":["body","diversity"]},"ear_tone1":{"unicode":"1f442-1f3fb","unicode_alt":"","code_decimal":"👂🏻","name":"ear tone 1","shortname":":ear_tone1:","category":"people","emoji_order":"1267","aliases":[],"aliases_ascii":[],"keywords":[]},"ear_tone2":{"unicode":"1f442-1f3fc","unicode_alt":"","code_decimal":"👂🏼","name":"ear tone 2","shortname":":ear_tone2:","category":"people","emoji_order":"1268","aliases":[],"aliases_ascii":[],"keywords":[]},"ear_tone3":{"unicode":"1f442-1f3fd","unicode_alt":"","code_decimal":"👂🏽","name":"ear tone 3","shortname":":ear_tone3:","category":"people","emoji_order":"1269","aliases":[],"aliases_ascii":[],"keywords":[]},"ear_tone4":{"unicode":"1f442-1f3fe","unicode_alt":"","code_decimal":"👂🏾","name":"ear tone 4","shortname":":ear_tone4:","category":"people","emoji_order":"1270","aliases":[],"aliases_ascii":[],"keywords":[]},"ear_tone5":{"unicode":"1f442-1f3ff","unicode_alt":"","code_decimal":"👂🏿","name":"ear tone 5","shortname":":ear_tone5:","category":"people","emoji_order":"1271","aliases":[],"aliases_ascii":[],"keywords":[]},"nose":{"unicode":"1f443","unicode_alt":"","code_decimal":"👃","name":"nose","shortname":":nose:","category":"people","emoji_order":"1272","aliases":[],"aliases_ascii":[],"keywords":["body","diversity"]},"nose_tone1":{"unicode":"1f443-1f3fb","unicode_alt":"","code_decimal":"👃🏻","name":"nose tone 1","shortname":":nose_tone1:","category":"people","emoji_order":"1273","aliases":[],"aliases_ascii":[],"keywords":[]},"nose_tone2":{"unicode":"1f443-1f3fc","unicode_alt":"","code_decimal":"👃🏼","name":"nose tone 2","shortname":":nose_tone2:","category":"people","emoji_order":"1274","aliases":[],"aliases_ascii":[],"keywords":[]},"nose_tone3":{"unicode":"1f443-1f3fd","unicode_alt":"","code_decimal":"👃🏽","name":"nose tone 3","shortname":":nose_tone3:","category":"people","emoji_order":"1275","aliases":[],"aliases_ascii":[],"keywords":[]},"nose_tone4":{"unicode":"1f443-1f3fe","unicode_alt":"","code_decimal":"👃🏾","name":"nose tone 4","shortname":":nose_tone4:","category":"people","emoji_order":"1276","aliases":[],"aliases_ascii":[],"keywords":[]},"nose_tone5":{"unicode":"1f443-1f3ff","unicode_alt":"","code_decimal":"👃🏿","name":"nose tone 5","shortname":":nose_tone5:","category":"people","emoji_order":"1277","aliases":[],"aliases_ascii":[],"keywords":[]},"footprints":{"unicode":"1f463","unicode_alt":"","code_decimal":"👣","name":"footprints","shortname":":footprints:","category":"people","emoji_order":"1278","aliases":[],"aliases_ascii":[],"keywords":[]},"eyes":{"unicode":"1f440","unicode_alt":"","code_decimal":"👀","name":"eyes","shortname":":eyes:","category":"people","emoji_order":"1279","aliases":[],"aliases_ascii":[],"keywords":["body","eyes"]},"eye":{"unicode":"1f441","unicode_alt":"1f441-fe0f","code_decimal":"👁","name":"eye","shortname":":eye:","category":"people","emoji_order":"1280","aliases":[],"aliases_ascii":[],"keywords":["body","eyes"]},"eye_in_speech_bubble":{"unicode":"1f441-1f5e8","unicode_alt":"1f441-200d-1f5e8","code_decimal":"👁🗨","name":"eye in speech bubble","shortname":":eye_in_speech_bubble:","category":"symbols","emoji_order":"1281","aliases":[],"aliases_ascii":[],"keywords":["object","symbol","eyes","talk"]},"tongue":{"unicode":"1f445","unicode_alt":"","code_decimal":"👅","name":"tongue","shortname":":tongue:","category":"people","emoji_order":"1282","aliases":[],"aliases_ascii":[],"keywords":["body","sexy","lip"]},"lips":{"unicode":"1f444","unicode_alt":"","code_decimal":"👄","name":"mouth","shortname":":lips:","category":"people","emoji_order":"1283","aliases":[],"aliases_ascii":[],"keywords":["women","body","sexy","lip"]},"kiss":{"unicode":"1f48b","unicode_alt":"","code_decimal":"💋","name":"kiss mark","shortname":":kiss:","category":"people","emoji_order":"1284","aliases":[],"aliases_ascii":[],"keywords":["women","love","sexy","lip","beautiful","girls night"]},"cupid":{"unicode":"1f498","unicode_alt":"","code_decimal":"💘","name":"heart with arrow","shortname":":cupid:","category":"symbols","emoji_order":"1285","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"heart":{"unicode":"2764","unicode_alt":"2764-fe0f","code_decimal":"❤","name":"heavy black heart","shortname":":heart:","category":"symbols","emoji_order":"1286","aliases":[],"aliases_ascii":["<3"],"keywords":["love","symbol","parties"]},"heartbeat":{"unicode":"1f493","unicode_alt":"","code_decimal":"💓","name":"beating heart","shortname":":heartbeat:","category":"symbols","emoji_order":"1287","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"broken_heart":{"unicode":"1f494","unicode_alt":"","code_decimal":"💔","name":"broken heart","shortname":":broken_heart:","category":"symbols","emoji_order":"1288","aliases":[],"aliases_ascii":["<\/3"],"keywords":["love","symbol","heartbreak"]},"two_hearts":{"unicode":"1f495","unicode_alt":"","code_decimal":"💕","name":"two hearts","shortname":":two_hearts:","category":"symbols","emoji_order":"1289","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"sparkling_heart":{"unicode":"1f496","unicode_alt":"","code_decimal":"💖","name":"sparkling heart","shortname":":sparkling_heart:","category":"symbols","emoji_order":"1290","aliases":[],"aliases_ascii":[],"keywords":["love","symbol","girls night"]},"heartpulse":{"unicode":"1f497","unicode_alt":"","code_decimal":"💗","name":"growing heart","shortname":":heartpulse:","category":"symbols","emoji_order":"1291","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"blue_heart":{"unicode":"1f499","unicode_alt":"","code_decimal":"💙","name":"blue heart","shortname":":blue_heart:","category":"symbols","emoji_order":"1292","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"green_heart":{"unicode":"1f49a","unicode_alt":"","code_decimal":"💚","name":"green heart","shortname":":green_heart:","category":"symbols","emoji_order":"1293","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"yellow_heart":{"unicode":"1f49b","unicode_alt":"","code_decimal":"💛","name":"yellow heart","shortname":":yellow_heart:","category":"symbols","emoji_order":"1294","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"purple_heart":{"unicode":"1f49c","unicode_alt":"","code_decimal":"💜","name":"purple heart","shortname":":purple_heart:","category":"symbols","emoji_order":"1295","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"black_heart":{"unicode":"1f5a4","unicode_alt":"","code_decimal":"🖤","name":"black heart","shortname":":black_heart:","category":"symbols","emoji_order":"1296","aliases":[],"aliases_ascii":[],"keywords":[]},"gift_heart":{"unicode":"1f49d","unicode_alt":"","code_decimal":"💝","name":"heart with ribbon","shortname":":gift_heart:","category":"symbols","emoji_order":"1297","aliases":[],"aliases_ascii":[],"keywords":["love","symbol","condolence"]},"revolving_hearts":{"unicode":"1f49e","unicode_alt":"","code_decimal":"💞","name":"revolving hearts","shortname":":revolving_hearts:","category":"symbols","emoji_order":"1298","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"heart_decoration":{"unicode":"1f49f","unicode_alt":"","code_decimal":"💟","name":"heart decoration","shortname":":heart_decoration:","category":"symbols","emoji_order":"1299","aliases":[],"aliases_ascii":[],"keywords":["love","symbol"]},"heart_exclamation":{"unicode":"2763","unicode_alt":"2763-fe0f","code_decimal":"❣","name":"heavy heart exclamation mark ornament","shortname":":heart_exclamation:","category":"symbols","emoji_order":"1300","aliases":[":heavy_heart_exclamation_mark_ornament:"],"aliases_ascii":[],"keywords":["love","symbol"]},"love_letter":{"unicode":"1f48c","unicode_alt":"","code_decimal":"💌","name":"love letter","shortname":":love_letter:","category":"objects","emoji_order":"1301","aliases":[],"aliases_ascii":[],"keywords":["object"]},"zzz":{"unicode":"1f4a4","unicode_alt":"","code_decimal":"💤","name":"sleeping symbol","shortname":":zzz:","category":"people","emoji_order":"1302","aliases":[],"aliases_ascii":[],"keywords":["tired","goodnight"]},"anger":{"unicode":"1f4a2","unicode_alt":"","code_decimal":"💢","name":"anger symbol","shortname":":anger:","category":"symbols","emoji_order":"1303","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"bomb":{"unicode":"1f4a3","unicode_alt":"","code_decimal":"💣","name":"bomb","shortname":":bomb:","category":"objects","emoji_order":"1304","aliases":[],"aliases_ascii":[],"keywords":["object","weapon","dead","blast"]},"boom":{"unicode":"1f4a5","unicode_alt":"","code_decimal":"💥","name":"collision symbol","shortname":":boom:","category":"symbols","emoji_order":"1305","aliases":[],"aliases_ascii":[],"keywords":["symbol","blast"]},"sweat_drops":{"unicode":"1f4a6","unicode_alt":"","code_decimal":"💦","name":"splashing sweat symbol","shortname":":sweat_drops:","category":"nature","emoji_order":"1306","aliases":[],"aliases_ascii":[],"keywords":["rain","stressed","sweat"]},"dash":{"unicode":"1f4a8","unicode_alt":"","code_decimal":"💨","name":"dash symbol","shortname":":dash:","category":"nature","emoji_order":"1307","aliases":[],"aliases_ascii":[],"keywords":["cloud","cold","smoking"]},"dizzy":{"unicode":"1f4ab","unicode_alt":"","code_decimal":"💫","name":"dizzy symbol","shortname":":dizzy:","category":"symbols","emoji_order":"1308","aliases":[],"aliases_ascii":[],"keywords":["star","symbol"]},"speech_balloon":{"unicode":"1f4ac","unicode_alt":"","code_decimal":"💬","name":"speech balloon","shortname":":speech_balloon:","category":"symbols","emoji_order":"1309","aliases":[],"aliases_ascii":[],"keywords":["symbol","free speech"]},"speech_left":{"unicode":"1f5e8","unicode_alt":"1f5e8-fe0f","code_decimal":"🗨","name":"left speech bubble","shortname":":speech_left:","category":"symbols","emoji_order":"1310","aliases":[":left_speech_bubble:"],"aliases_ascii":[],"keywords":[]},"anger_right":{"unicode":"1f5ef","unicode_alt":"1f5ef-fe0f","code_decimal":"🗯","name":"right anger bubble","shortname":":anger_right:","category":"symbols","emoji_order":"1311","aliases":[":right_anger_bubble:"],"aliases_ascii":[],"keywords":["symbol"]},"thought_balloon":{"unicode":"1f4ad","unicode_alt":"","code_decimal":"💭","name":"thought balloon","shortname":":thought_balloon:","category":"symbols","emoji_order":"1312","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"hole":{"unicode":"1f573","unicode_alt":"1f573-fe0f","code_decimal":"🕳","name":"hole","shortname":":hole:","category":"objects","emoji_order":"1313","aliases":[],"aliases_ascii":[],"keywords":["object"]},"eyeglasses":{"unicode":"1f453","unicode_alt":"","code_decimal":"👓","name":"eyeglasses","shortname":":eyeglasses:","category":"people","emoji_order":"1314","aliases":[],"aliases_ascii":[],"keywords":["fashion","glasses","accessories"]},"dark_sunglasses":{"unicode":"1f576","unicode_alt":"1f576-fe0f","code_decimal":"🕶","name":"dark sunglasses","shortname":":dark_sunglasses:","category":"people","emoji_order":"1315","aliases":[],"aliases_ascii":[],"keywords":["fashion","glasses","accessories"]},"necktie":{"unicode":"1f454","unicode_alt":"","code_decimal":"👔","name":"necktie","shortname":":necktie:","category":"people","emoji_order":"1316","aliases":[],"aliases_ascii":[],"keywords":["fashion"]},"shirt":{"unicode":"1f455","unicode_alt":"","code_decimal":"👕","name":"t-shirt","shortname":":shirt:","category":"people","emoji_order":"1317","aliases":[],"aliases_ascii":[],"keywords":["fashion"]},"jeans":{"unicode":"1f456","unicode_alt":"","code_decimal":"👖","name":"jeans","shortname":":jeans:","category":"people","emoji_order":"1318","aliases":[],"aliases_ascii":[],"keywords":["fashion"]},"dress":{"unicode":"1f457","unicode_alt":"","code_decimal":"👗","name":"dress","shortname":":dress:","category":"people","emoji_order":"1319","aliases":[],"aliases_ascii":[],"keywords":["women","fashion","sexy","girls night"]},"kimono":{"unicode":"1f458","unicode_alt":"","code_decimal":"👘","name":"kimono","shortname":":kimono:","category":"people","emoji_order":"1320","aliases":[],"aliases_ascii":[],"keywords":["fashion"]},"bikini":{"unicode":"1f459","unicode_alt":"","code_decimal":"👙","name":"bikini","shortname":":bikini:","category":"people","emoji_order":"1321","aliases":[],"aliases_ascii":[],"keywords":["women","fashion","sexy","vacation","tropical","swim"]},"womans_clothes":{"unicode":"1f45a","unicode_alt":"","code_decimal":"👚","name":"womans clothes","shortname":":womans_clothes:","category":"people","emoji_order":"1322","aliases":[],"aliases_ascii":[],"keywords":["women","fashion"]},"purse":{"unicode":"1f45b","unicode_alt":"","code_decimal":"👛","name":"purse","shortname":":purse:","category":"people","emoji_order":"1323","aliases":[],"aliases_ascii":[],"keywords":["bag","women","fashion","accessories","money"]},"handbag":{"unicode":"1f45c","unicode_alt":"","code_decimal":"👜","name":"handbag","shortname":":handbag:","category":"people","emoji_order":"1324","aliases":[],"aliases_ascii":[],"keywords":["bag","women","fashion","vacation","accessories"]},"pouch":{"unicode":"1f45d","unicode_alt":"","code_decimal":"👝","name":"pouch","shortname":":pouch:","category":"people","emoji_order":"1325","aliases":[],"aliases_ascii":[],"keywords":["bag","women","fashion","accessories"]},"shopping_bags":{"unicode":"1f6cd","unicode_alt":"1f6cd-fe0f","code_decimal":"🛍","name":"shopping bags","shortname":":shopping_bags:","category":"objects","emoji_order":"1326","aliases":[],"aliases_ascii":[],"keywords":["object","birthday","parties"]},"school_satchel":{"unicode":"1f392","unicode_alt":"","code_decimal":"🎒","name":"school satchel","shortname":":school_satchel:","category":"people","emoji_order":"1327","aliases":[],"aliases_ascii":[],"keywords":["bag","fashion","office","vacation","accessories"]},"mans_shoe":{"unicode":"1f45e","unicode_alt":"","code_decimal":"👞","name":"mans shoe","shortname":":mans_shoe:","category":"people","emoji_order":"1328","aliases":[],"aliases_ascii":[],"keywords":["fashion","shoe","accessories"]},"athletic_shoe":{"unicode":"1f45f","unicode_alt":"","code_decimal":"👟","name":"athletic shoe","shortname":":athletic_shoe:","category":"people","emoji_order":"1329","aliases":[],"aliases_ascii":[],"keywords":["fashion","shoe","accessories","boys night"]},"high_heel":{"unicode":"1f460","unicode_alt":"","code_decimal":"👠","name":"high-heeled shoe","shortname":":high_heel:","category":"people","emoji_order":"1330","aliases":[],"aliases_ascii":[],"keywords":["women","fashion","shoe","sexy","accessories","girls night"]},"sandal":{"unicode":"1f461","unicode_alt":"","code_decimal":"👡","name":"womans sandal","shortname":":sandal:","category":"people","emoji_order":"1331","aliases":[],"aliases_ascii":[],"keywords":["fashion","shoe","accessories"]},"boot":{"unicode":"1f462","unicode_alt":"","code_decimal":"👢","name":"womans boots","shortname":":boot:","category":"people","emoji_order":"1332","aliases":[],"aliases_ascii":[],"keywords":["women","fashion","shoe","sexy","accessories"]},"crown":{"unicode":"1f451","unicode_alt":"","code_decimal":"👑","name":"crown","shortname":":crown:","category":"people","emoji_order":"1333","aliases":[],"aliases_ascii":[],"keywords":["object","gem","accessories"]},"womans_hat":{"unicode":"1f452","unicode_alt":"","code_decimal":"👒","name":"womans hat","shortname":":womans_hat:","category":"people","emoji_order":"1334","aliases":[],"aliases_ascii":[],"keywords":["women","fashion","accessories"]},"tophat":{"unicode":"1f3a9","unicode_alt":"","code_decimal":"🎩","name":"top hat","shortname":":tophat:","category":"people","emoji_order":"1335","aliases":[],"aliases_ascii":[],"keywords":["hat","fashion","accessories"]},"mortar_board":{"unicode":"1f393","unicode_alt":"","code_decimal":"🎓","name":"graduation cap","shortname":":mortar_board:","category":"people","emoji_order":"1336","aliases":[],"aliases_ascii":[],"keywords":["hat","office","accessories"]},"helmet_with_cross":{"unicode":"26d1","unicode_alt":"26d1-fe0f","code_decimal":"⛑","name":"helmet with white cross","shortname":":helmet_with_cross:","category":"people","emoji_order":"1337","aliases":[":helmet_with_white_cross:"],"aliases_ascii":[],"keywords":["object","hat","accessories","job"]},"prayer_beads":{"unicode":"1f4ff","unicode_alt":"","code_decimal":"📿","name":"prayer beads","shortname":":prayer_beads:","category":"objects","emoji_order":"1338","aliases":[],"aliases_ascii":[],"keywords":["object","rosary"]},"lipstick":{"unicode":"1f484","unicode_alt":"","code_decimal":"💄","name":"lipstick","shortname":":lipstick:","category":"people","emoji_order":"1339","aliases":[],"aliases_ascii":[],"keywords":["object","women","fashion","sexy","lip"]},"ring":{"unicode":"1f48d","unicode_alt":"","code_decimal":"💍","name":"ring","shortname":":ring:","category":"people","emoji_order":"1340","aliases":[],"aliases_ascii":[],"keywords":["wedding","object","fashion","gem","accessories"]},"gem":{"unicode":"1f48e","unicode_alt":"","code_decimal":"💎","name":"gem stone","shortname":":gem:","category":"objects","emoji_order":"1341","aliases":[],"aliases_ascii":[],"keywords":["object","gem"]},"monkey_face":{"unicode":"1f435","unicode_alt":"","code_decimal":"🐵","name":"monkey face","shortname":":monkey_face:","category":"nature","emoji_order":"1342","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"monkey":{"unicode":"1f412","unicode_alt":"","code_decimal":"🐒","name":"monkey","shortname":":monkey:","category":"nature","emoji_order":"1343","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"gorilla":{"unicode":"1f98d","unicode_alt":"","code_decimal":"🦍","name":"gorilla","shortname":":gorilla:","category":"nature","emoji_order":"1344","aliases":[],"aliases_ascii":[],"keywords":[]},"dog":{"unicode":"1f436","unicode_alt":"","code_decimal":"🐶","name":"dog face","shortname":":dog:","category":"nature","emoji_order":"1345","aliases":[],"aliases_ascii":[],"keywords":["dog","pug","animal"]},"dog2":{"unicode":"1f415","unicode_alt":"","code_decimal":"🐕","name":"dog","shortname":":dog2:","category":"nature","emoji_order":"1346","aliases":[],"aliases_ascii":[],"keywords":["dog","pug","animal"]},"poodle":{"unicode":"1f429","unicode_alt":"","code_decimal":"🐩","name":"poodle","shortname":":poodle:","category":"nature","emoji_order":"1347","aliases":[],"aliases_ascii":[],"keywords":["dog","animal"]},"wolf":{"unicode":"1f43a","unicode_alt":"","code_decimal":"🐺","name":"wolf face","shortname":":wolf:","category":"nature","emoji_order":"1348","aliases":[],"aliases_ascii":[],"keywords":["wildlife","roar","animal"]},"fox":{"unicode":"1f98a","unicode_alt":"","code_decimal":"🦊","name":"fox face","shortname":":fox:","category":"nature","emoji_order":"1349","aliases":[":fox_face:"],"aliases_ascii":[],"keywords":[]},"cat":{"unicode":"1f431","unicode_alt":"","code_decimal":"🐱","name":"cat face","shortname":":cat:","category":"nature","emoji_order":"1350","aliases":[],"aliases_ascii":[],"keywords":["halloween","vagina","cat","animal"]},"cat2":{"unicode":"1f408","unicode_alt":"","code_decimal":"🐈","name":"cat","shortname":":cat2:","category":"nature","emoji_order":"1351","aliases":[],"aliases_ascii":[],"keywords":["halloween","cat","animal"]},"lion_face":{"unicode":"1f981","unicode_alt":"","code_decimal":"🦁","name":"lion face","shortname":":lion_face:","category":"nature","emoji_order":"1352","aliases":[":lion:"],"aliases_ascii":[],"keywords":["wildlife","roar","cat","animal"]},"tiger":{"unicode":"1f42f","unicode_alt":"","code_decimal":"🐯","name":"tiger face","shortname":":tiger:","category":"nature","emoji_order":"1353","aliases":[],"aliases_ascii":[],"keywords":["wildlife","roar","cat","animal"]},"tiger2":{"unicode":"1f405","unicode_alt":"","code_decimal":"🐅","name":"tiger","shortname":":tiger2:","category":"nature","emoji_order":"1354","aliases":[],"aliases_ascii":[],"keywords":["wildlife","roar","animal"]},"leopard":{"unicode":"1f406","unicode_alt":"","code_decimal":"🐆","name":"leopard","shortname":":leopard:","category":"nature","emoji_order":"1355","aliases":[],"aliases_ascii":[],"keywords":["wildlife","roar","animal"]},"horse":{"unicode":"1f434","unicode_alt":"","code_decimal":"🐴","name":"horse face","shortname":":horse:","category":"nature","emoji_order":"1356","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"racehorse":{"unicode":"1f40e","unicode_alt":"","code_decimal":"🐎","name":"horse","shortname":":racehorse:","category":"nature","emoji_order":"1357","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"deer":{"unicode":"1f98c","unicode_alt":"","code_decimal":"🦌","name":"deer","shortname":":deer:","category":"nature","emoji_order":"1358","aliases":[],"aliases_ascii":[],"keywords":[]},"unicorn":{"unicode":"1f984","unicode_alt":"","code_decimal":"🦄","name":"unicorn face","shortname":":unicorn:","category":"nature","emoji_order":"1359","aliases":[":unicorn_face:"],"aliases_ascii":[],"keywords":["animal"]},"cow":{"unicode":"1f42e","unicode_alt":"","code_decimal":"🐮","name":"cow face","shortname":":cow:","category":"nature","emoji_order":"1360","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"ox":{"unicode":"1f402","unicode_alt":"","code_decimal":"🐂","name":"ox","shortname":":ox:","category":"nature","emoji_order":"1361","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"water_buffalo":{"unicode":"1f403","unicode_alt":"","code_decimal":"🐃","name":"water buffalo","shortname":":water_buffalo:","category":"nature","emoji_order":"1362","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"cow2":{"unicode":"1f404","unicode_alt":"","code_decimal":"🐄","name":"cow","shortname":":cow2:","category":"nature","emoji_order":"1363","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"pig":{"unicode":"1f437","unicode_alt":"","code_decimal":"🐷","name":"pig face","shortname":":pig:","category":"nature","emoji_order":"1364","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"pig2":{"unicode":"1f416","unicode_alt":"","code_decimal":"🐖","name":"pig","shortname":":pig2:","category":"nature","emoji_order":"1365","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"boar":{"unicode":"1f417","unicode_alt":"","code_decimal":"🐗","name":"boar","shortname":":boar:","category":"nature","emoji_order":"1366","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"pig_nose":{"unicode":"1f43d","unicode_alt":"","code_decimal":"🐽","name":"pig nose","shortname":":pig_nose:","category":"nature","emoji_order":"1367","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"ram":{"unicode":"1f40f","unicode_alt":"","code_decimal":"🐏","name":"ram","shortname":":ram:","category":"nature","emoji_order":"1368","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"sheep":{"unicode":"1f411","unicode_alt":"","code_decimal":"🐑","name":"sheep","shortname":":sheep:","category":"nature","emoji_order":"1369","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"goat":{"unicode":"1f410","unicode_alt":"","code_decimal":"🐐","name":"goat","shortname":":goat:","category":"nature","emoji_order":"1370","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"dromedary_camel":{"unicode":"1f42a","unicode_alt":"","code_decimal":"🐪","name":"dromedary camel","shortname":":dromedary_camel:","category":"nature","emoji_order":"1371","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"camel":{"unicode":"1f42b","unicode_alt":"","code_decimal":"🐫","name":"bactrian camel","shortname":":camel:","category":"nature","emoji_order":"1372","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal","hump day"]},"elephant":{"unicode":"1f418","unicode_alt":"","code_decimal":"🐘","name":"elephant","shortname":":elephant:","category":"nature","emoji_order":"1373","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"rhino":{"unicode":"1f98f","unicode_alt":"","code_decimal":"🦏","name":"rhinoceros","shortname":":rhino:","category":"nature","emoji_order":"1374","aliases":[":rhinoceros:"],"aliases_ascii":[],"keywords":[]},"mouse":{"unicode":"1f42d","unicode_alt":"","code_decimal":"🐭","name":"mouse face","shortname":":mouse:","category":"nature","emoji_order":"1375","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"mouse2":{"unicode":"1f401","unicode_alt":"","code_decimal":"🐁","name":"mouse","shortname":":mouse2:","category":"nature","emoji_order":"1376","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"rat":{"unicode":"1f400","unicode_alt":"","code_decimal":"🐀","name":"rat","shortname":":rat:","category":"nature","emoji_order":"1377","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"hamster":{"unicode":"1f439","unicode_alt":"","code_decimal":"🐹","name":"hamster face","shortname":":hamster:","category":"nature","emoji_order":"1378","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"rabbit":{"unicode":"1f430","unicode_alt":"","code_decimal":"🐰","name":"rabbit face","shortname":":rabbit:","category":"nature","emoji_order":"1379","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"rabbit2":{"unicode":"1f407","unicode_alt":"","code_decimal":"🐇","name":"rabbit","shortname":":rabbit2:","category":"nature","emoji_order":"1380","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"chipmunk":{"unicode":"1f43f","unicode_alt":"1f43f-fe0f","code_decimal":"🐿","name":"chipmunk","shortname":":chipmunk:","category":"nature","emoji_order":"1381","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"bat":{"unicode":"1f987","unicode_alt":"","code_decimal":"🦇","name":"bat","shortname":":bat:","category":"nature","emoji_order":"1382","aliases":[],"aliases_ascii":[],"keywords":[]},"bear":{"unicode":"1f43b","unicode_alt":"","code_decimal":"🐻","name":"bear face","shortname":":bear:","category":"nature","emoji_order":"1383","aliases":[],"aliases_ascii":[],"keywords":["wildlife","roar","animal"]},"koala":{"unicode":"1f428","unicode_alt":"","code_decimal":"🐨","name":"koala","shortname":":koala:","category":"nature","emoji_order":"1384","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"panda_face":{"unicode":"1f43c","unicode_alt":"","code_decimal":"🐼","name":"panda face","shortname":":panda_face:","category":"nature","emoji_order":"1385","aliases":[],"aliases_ascii":[],"keywords":["wildlife","roar","animal"]},"feet":{"unicode":"1f43e","unicode_alt":"","code_decimal":"🐾","name":"paw prints","shortname":":feet:","category":"nature","emoji_order":"1386","aliases":[":paw_prints:"],"aliases_ascii":[],"keywords":["animal"]},"turkey":{"unicode":"1f983","unicode_alt":"","code_decimal":"🦃","name":"turkey","shortname":":turkey:","category":"nature","emoji_order":"1387","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"chicken":{"unicode":"1f414","unicode_alt":"","code_decimal":"🐔","name":"chicken","shortname":":chicken:","category":"nature","emoji_order":"1388","aliases":[],"aliases_ascii":[],"keywords":["animal","chicken"]},"rooster":{"unicode":"1f413","unicode_alt":"","code_decimal":"🐓","name":"rooster","shortname":":rooster:","category":"nature","emoji_order":"1389","aliases":[],"aliases_ascii":[],"keywords":["animal"]},"hatching_chick":{"unicode":"1f423","unicode_alt":"","code_decimal":"🐣","name":"hatching chick","shortname":":hatching_chick:","category":"nature","emoji_order":"1390","aliases":[],"aliases_ascii":[],"keywords":["animal","chicken"]},"baby_chick":{"unicode":"1f424","unicode_alt":"","code_decimal":"🐤","name":"baby chick","shortname":":baby_chick:","category":"nature","emoji_order":"1391","aliases":[],"aliases_ascii":[],"keywords":["animal","chicken"]},"hatched_chick":{"unicode":"1f425","unicode_alt":"","code_decimal":"🐥","name":"front-facing baby chick","shortname":":hatched_chick:","category":"nature","emoji_order":"1392","aliases":[],"aliases_ascii":[],"keywords":["animal","chicken"]},"bird":{"unicode":"1f426","unicode_alt":"","code_decimal":"🐦","name":"bird","shortname":":bird:","category":"nature","emoji_order":"1393","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"penguin":{"unicode":"1f427","unicode_alt":"","code_decimal":"🐧","name":"penguin","shortname":":penguin:","category":"nature","emoji_order":"1394","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"dove":{"unicode":"1f54a","unicode_alt":"1f54a-fe0f","code_decimal":"🕊","name":"dove of peace","shortname":":dove:","category":"nature","emoji_order":"1395","aliases":[":dove_of_peace:"],"aliases_ascii":[],"keywords":["animal"]},"eagle":{"unicode":"1f985","unicode_alt":"","code_decimal":"🦅","name":"eagle","shortname":":eagle:","category":"nature","emoji_order":"1396","aliases":[],"aliases_ascii":[],"keywords":[]},"duck":{"unicode":"1f986","unicode_alt":"","code_decimal":"🦆","name":"duck","shortname":":duck:","category":"nature","emoji_order":"1397","aliases":[],"aliases_ascii":[],"keywords":[]},"owl":{"unicode":"1f989","unicode_alt":"","code_decimal":"🦉","name":"owl","shortname":":owl:","category":"nature","emoji_order":"1398","aliases":[],"aliases_ascii":[],"keywords":[]},"frog":{"unicode":"1f438","unicode_alt":"","code_decimal":"🐸","name":"frog face","shortname":":frog:","category":"nature","emoji_order":"1399","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"crocodile":{"unicode":"1f40a","unicode_alt":"","code_decimal":"🐊","name":"crocodile","shortname":":crocodile:","category":"nature","emoji_order":"1400","aliases":[],"aliases_ascii":[],"keywords":["wildlife","reptile","animal"]},"turtle":{"unicode":"1f422","unicode_alt":"","code_decimal":"🐢","name":"turtle","shortname":":turtle:","category":"nature","emoji_order":"1401","aliases":[],"aliases_ascii":[],"keywords":["wildlife","reptile","animal"]},"lizard":{"unicode":"1f98e","unicode_alt":"","code_decimal":"🦎","name":"lizard","shortname":":lizard:","category":"nature","emoji_order":"1402","aliases":[],"aliases_ascii":[],"keywords":[]},"snake":{"unicode":"1f40d","unicode_alt":"","code_decimal":"🐍","name":"snake","shortname":":snake:","category":"nature","emoji_order":"1403","aliases":[],"aliases_ascii":[],"keywords":["wildlife","reptile","animal","creationism"]},"dragon_face":{"unicode":"1f432","unicode_alt":"","code_decimal":"🐲","name":"dragon face","shortname":":dragon_face:","category":"nature","emoji_order":"1404","aliases":[],"aliases_ascii":[],"keywords":["roar","monster","reptile","animal"]},"dragon":{"unicode":"1f409","unicode_alt":"","code_decimal":"🐉","name":"dragon","shortname":":dragon:","category":"nature","emoji_order":"1405","aliases":[],"aliases_ascii":[],"keywords":["roar","reptile","animal"]},"whale":{"unicode":"1f433","unicode_alt":"","code_decimal":"🐳","name":"spouting whale","shortname":":whale:","category":"nature","emoji_order":"1406","aliases":[],"aliases_ascii":[],"keywords":["wildlife","tropical","whales","animal"]},"whale2":{"unicode":"1f40b","unicode_alt":"","code_decimal":"🐋","name":"whale","shortname":":whale2:","category":"nature","emoji_order":"1407","aliases":[],"aliases_ascii":[],"keywords":["wildlife","tropical","whales","animal"]},"dolphin":{"unicode":"1f42c","unicode_alt":"","code_decimal":"🐬","name":"dolphin","shortname":":dolphin:","category":"nature","emoji_order":"1408","aliases":[],"aliases_ascii":[],"keywords":["wildlife","tropical","animal"]},"fish":{"unicode":"1f41f","unicode_alt":"","code_decimal":"🐟","name":"fish","shortname":":fish:","category":"nature","emoji_order":"1409","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"tropical_fish":{"unicode":"1f420","unicode_alt":"","code_decimal":"🐠","name":"tropical fish","shortname":":tropical_fish:","category":"nature","emoji_order":"1410","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"blowfish":{"unicode":"1f421","unicode_alt":"","code_decimal":"🐡","name":"blowfish","shortname":":blowfish:","category":"nature","emoji_order":"1411","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"shark":{"unicode":"1f988","unicode_alt":"","code_decimal":"🦈","name":"shark","shortname":":shark:","category":"nature","emoji_order":"1412","aliases":[],"aliases_ascii":[],"keywords":[]},"octopus":{"unicode":"1f419","unicode_alt":"","code_decimal":"🐙","name":"octopus","shortname":":octopus:","category":"nature","emoji_order":"1413","aliases":[],"aliases_ascii":[],"keywords":["wildlife","animal"]},"shell":{"unicode":"1f41a","unicode_alt":"","code_decimal":"🐚","name":"spiral shell","shortname":":shell:","category":"nature","emoji_order":"1414","aliases":[],"aliases_ascii":[],"keywords":[]},"crab":{"unicode":"1f980","unicode_alt":"","code_decimal":"🦀","name":"crab","shortname":":crab:","category":"nature","emoji_order":"1415","aliases":[],"aliases_ascii":[],"keywords":["tropical","animal"]},"shrimp":{"unicode":"1f990","unicode_alt":"","code_decimal":"🦐","name":"shrimp","shortname":":shrimp:","category":"nature","emoji_order":"1416","aliases":[],"aliases_ascii":[],"keywords":[]},"squid":{"unicode":"1f991","unicode_alt":"","code_decimal":"🦑","name":"squid","shortname":":squid:","category":"nature","emoji_order":"1417","aliases":[],"aliases_ascii":[],"keywords":[]},"butterfly":{"unicode":"1f98b","unicode_alt":"","code_decimal":"🦋","name":"butterfly","shortname":":butterfly:","category":"nature","emoji_order":"1418","aliases":[],"aliases_ascii":[],"keywords":[]},"snail":{"unicode":"1f40c","unicode_alt":"","code_decimal":"🐌","name":"snail","shortname":":snail:","category":"nature","emoji_order":"1419","aliases":[],"aliases_ascii":[],"keywords":["insects","animal"]},"bug":{"unicode":"1f41b","unicode_alt":"","code_decimal":"🐛","name":"bug","shortname":":bug:","category":"nature","emoji_order":"1420","aliases":[],"aliases_ascii":[],"keywords":["insects","animal"]},"ant":{"unicode":"1f41c","unicode_alt":"","code_decimal":"🐜","name":"ant","shortname":":ant:","category":"nature","emoji_order":"1421","aliases":[],"aliases_ascii":[],"keywords":["insects","animal"]},"bee":{"unicode":"1f41d","unicode_alt":"","code_decimal":"🐝","name":"honeybee","shortname":":bee:","category":"nature","emoji_order":"1422","aliases":[],"aliases_ascii":[],"keywords":["insects","animal"]},"beetle":{"unicode":"1f41e","unicode_alt":"","code_decimal":"🐞","name":"lady beetle","shortname":":beetle:","category":"nature","emoji_order":"1423","aliases":[],"aliases_ascii":[],"keywords":["insects","animal"]},"spider":{"unicode":"1f577","unicode_alt":"1f577-fe0f","code_decimal":"🕷","name":"spider","shortname":":spider:","category":"nature","emoji_order":"1424","aliases":[],"aliases_ascii":[],"keywords":["insects","halloween","animal"]},"spider_web":{"unicode":"1f578","unicode_alt":"1f578-fe0f","code_decimal":"🕸","name":"spider web","shortname":":spider_web:","category":"nature","emoji_order":"1425","aliases":[],"aliases_ascii":[],"keywords":["halloween"]},"scorpion":{"unicode":"1f982","unicode_alt":"","code_decimal":"🦂","name":"scorpion","shortname":":scorpion:","category":"nature","emoji_order":"1426","aliases":[],"aliases_ascii":[],"keywords":["insects","reptile","animal"]},"bouquet":{"unicode":"1f490","unicode_alt":"","code_decimal":"💐","name":"bouquet","shortname":":bouquet:","category":"nature","emoji_order":"1427","aliases":[],"aliases_ascii":[],"keywords":["nature","flower","plant","rip","condolence"]},"cherry_blossom":{"unicode":"1f338","unicode_alt":"","code_decimal":"🌸","name":"cherry blossom","shortname":":cherry_blossom:","category":"nature","emoji_order":"1428","aliases":[],"aliases_ascii":[],"keywords":["nature","flower","plant","tropical"]},"white_flower":{"unicode":"1f4ae","unicode_alt":"","code_decimal":"💮","name":"white flower","shortname":":white_flower:","category":"symbols","emoji_order":"1429","aliases":[],"aliases_ascii":[],"keywords":["flower","symbol"]},"rosette":{"unicode":"1f3f5","unicode_alt":"1f3f5-fe0f","code_decimal":"🏵","name":"rosette","shortname":":rosette:","category":"nature","emoji_order":"1430","aliases":[],"aliases_ascii":[],"keywords":["tropical"]},"rose":{"unicode":"1f339","unicode_alt":"","code_decimal":"🌹","name":"rose","shortname":":rose:","category":"nature","emoji_order":"1431","aliases":[],"aliases_ascii":[],"keywords":["nature","flower","plant","rip","condolence","beautiful"]},"wilted_rose":{"unicode":"1f940","unicode_alt":"","code_decimal":"🥀","name":"wilted flower","shortname":":wilted_rose:","category":"nature","emoji_order":"1432","aliases":[":wilted_flower:"],"aliases_ascii":[],"keywords":[]},"hibiscus":{"unicode":"1f33a","unicode_alt":"","code_decimal":"🌺","name":"hibiscus","shortname":":hibiscus:","category":"nature","emoji_order":"1433","aliases":[],"aliases_ascii":[],"keywords":["nature","flower","plant","tropical"]},"sunflower":{"unicode":"1f33b","unicode_alt":"","code_decimal":"🌻","name":"sunflower","shortname":":sunflower:","category":"nature","emoji_order":"1434","aliases":[],"aliases_ascii":[],"keywords":["nature","flower","plant"]},"blossom":{"unicode":"1f33c","unicode_alt":"","code_decimal":"🌼","name":"blossom","shortname":":blossom:","category":"nature","emoji_order":"1435","aliases":[],"aliases_ascii":[],"keywords":["nature","flower","plant"]},"tulip":{"unicode":"1f337","unicode_alt":"","code_decimal":"🌷","name":"tulip","shortname":":tulip:","category":"nature","emoji_order":"1436","aliases":[],"aliases_ascii":[],"keywords":["nature","flower","plant","vagina","girls night"]},"seedling":{"unicode":"1f331","unicode_alt":"","code_decimal":"🌱","name":"seedling","shortname":":seedling:","category":"nature","emoji_order":"1437","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","leaf"]},"evergreen_tree":{"unicode":"1f332","unicode_alt":"","code_decimal":"🌲","name":"evergreen tree","shortname":":evergreen_tree:","category":"nature","emoji_order":"1438","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","holidays","christmas","camp","trees"]},"deciduous_tree":{"unicode":"1f333","unicode_alt":"","code_decimal":"🌳","name":"deciduous tree","shortname":":deciduous_tree:","category":"nature","emoji_order":"1439","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","camp","trees"]},"palm_tree":{"unicode":"1f334","unicode_alt":"","code_decimal":"🌴","name":"palm tree","shortname":":palm_tree:","category":"nature","emoji_order":"1440","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","tropical","trees"]},"cactus":{"unicode":"1f335","unicode_alt":"","code_decimal":"🌵","name":"cactus","shortname":":cactus:","category":"nature","emoji_order":"1441","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","trees"]},"ear_of_rice":{"unicode":"1f33e","unicode_alt":"","code_decimal":"🌾","name":"ear of rice","shortname":":ear_of_rice:","category":"nature","emoji_order":"1442","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","leaf"]},"herb":{"unicode":"1f33f","unicode_alt":"","code_decimal":"🌿","name":"herb","shortname":":herb:","category":"nature","emoji_order":"1443","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","leaf"]},"shamrock":{"unicode":"2618","unicode_alt":"2618-fe0f","code_decimal":"☘","name":"shamrock","shortname":":shamrock:","category":"nature","emoji_order":"1444","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","luck","leaf"]},"four_leaf_clover":{"unicode":"1f340","unicode_alt":"","code_decimal":"🍀","name":"four leaf clover","shortname":":four_leaf_clover:","category":"nature","emoji_order":"1445","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","luck","leaf","sol"]},"maple_leaf":{"unicode":"1f341","unicode_alt":"","code_decimal":"🍁","name":"maple leaf","shortname":":maple_leaf:","category":"nature","emoji_order":"1446","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","leaf"]},"fallen_leaf":{"unicode":"1f342","unicode_alt":"","code_decimal":"🍂","name":"fallen leaf","shortname":":fallen_leaf:","category":"nature","emoji_order":"1447","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","leaf"]},"leaves":{"unicode":"1f343","unicode_alt":"","code_decimal":"🍃","name":"leaf fluttering in wind","shortname":":leaves:","category":"nature","emoji_order":"1448","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","leaf"]},"grapes":{"unicode":"1f347","unicode_alt":"","code_decimal":"🍇","name":"grapes","shortname":":grapes:","category":"food","emoji_order":"1449","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"melon":{"unicode":"1f348","unicode_alt":"","code_decimal":"🍈","name":"melon","shortname":":melon:","category":"food","emoji_order":"1450","aliases":[],"aliases_ascii":[],"keywords":["fruit","boobs","food"]},"watermelon":{"unicode":"1f349","unicode_alt":"","code_decimal":"🍉","name":"watermelon","shortname":":watermelon:","category":"food","emoji_order":"1451","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"tangerine":{"unicode":"1f34a","unicode_alt":"","code_decimal":"🍊","name":"tangerine","shortname":":tangerine:","category":"food","emoji_order":"1452","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"lemon":{"unicode":"1f34b","unicode_alt":"","code_decimal":"🍋","name":"lemon","shortname":":lemon:","category":"food","emoji_order":"1453","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"banana":{"unicode":"1f34c","unicode_alt":"","code_decimal":"🍌","name":"banana","shortname":":banana:","category":"food","emoji_order":"1454","aliases":[],"aliases_ascii":[],"keywords":["fruit","penis","food"]},"pineapple":{"unicode":"1f34d","unicode_alt":"","code_decimal":"🍍","name":"pineapple","shortname":":pineapple:","category":"food","emoji_order":"1455","aliases":[],"aliases_ascii":[],"keywords":["fruit","food","tropical"]},"apple":{"unicode":"1f34e","unicode_alt":"","code_decimal":"🍎","name":"red apple","shortname":":apple:","category":"food","emoji_order":"1456","aliases":[],"aliases_ascii":[],"keywords":["fruit","food","creationism"]},"green_apple":{"unicode":"1f34f","unicode_alt":"","code_decimal":"🍏","name":"green apple","shortname":":green_apple:","category":"food","emoji_order":"1457","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"pear":{"unicode":"1f350","unicode_alt":"","code_decimal":"🍐","name":"pear","shortname":":pear:","category":"food","emoji_order":"1458","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"peach":{"unicode":"1f351","unicode_alt":"","code_decimal":"🍑","name":"peach","shortname":":peach:","category":"food","emoji_order":"1459","aliases":[],"aliases_ascii":[],"keywords":["fruit","butt","food"]},"cherries":{"unicode":"1f352","unicode_alt":"","code_decimal":"🍒","name":"cherries","shortname":":cherries:","category":"food","emoji_order":"1460","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"strawberry":{"unicode":"1f353","unicode_alt":"","code_decimal":"🍓","name":"strawberry","shortname":":strawberry:","category":"food","emoji_order":"1461","aliases":[],"aliases_ascii":[],"keywords":["fruit","food"]},"kiwi":{"unicode":"1f95d","unicode_alt":"","code_decimal":"🥝","name":"kiwifruit","shortname":":kiwi:","category":"food","emoji_order":"1462","aliases":[":kiwifruit:"],"aliases_ascii":[],"keywords":[]},"tomato":{"unicode":"1f345","unicode_alt":"","code_decimal":"🍅","name":"tomato","shortname":":tomato:","category":"food","emoji_order":"1463","aliases":[],"aliases_ascii":[],"keywords":["fruit","vegetables","food"]},"avocado":{"unicode":"1f951","unicode_alt":"","code_decimal":"🥑","name":"avocado","shortname":":avocado:","category":"food","emoji_order":"1464","aliases":[],"aliases_ascii":[],"keywords":[]},"eggplant":{"unicode":"1f346","unicode_alt":"","code_decimal":"🍆","name":"aubergine","shortname":":eggplant:","category":"food","emoji_order":"1465","aliases":[],"aliases_ascii":[],"keywords":["vegetables","penis","food"]},"potato":{"unicode":"1f954","unicode_alt":"","code_decimal":"🥔","name":"potato","shortname":":potato:","category":"food","emoji_order":"1466","aliases":[],"aliases_ascii":[],"keywords":[]},"carrot":{"unicode":"1f955","unicode_alt":"","code_decimal":"🥕","name":"carrot","shortname":":carrot:","category":"food","emoji_order":"1467","aliases":[],"aliases_ascii":[],"keywords":[]},"corn":{"unicode":"1f33d","unicode_alt":"","code_decimal":"🌽","name":"ear of maize","shortname":":corn:","category":"food","emoji_order":"1468","aliases":[],"aliases_ascii":[],"keywords":["vegetables","food"]},"hot_pepper":{"unicode":"1f336","unicode_alt":"1f336-fe0f","code_decimal":"🌶","name":"hot pepper","shortname":":hot_pepper:","category":"food","emoji_order":"1469","aliases":[],"aliases_ascii":[],"keywords":["vegetables","food"]},"cucumber":{"unicode":"1f952","unicode_alt":"","code_decimal":"🥒","name":"cucumber","shortname":":cucumber:","category":"food","emoji_order":"1470","aliases":[],"aliases_ascii":[],"keywords":[]},"mushroom":{"unicode":"1f344","unicode_alt":"","code_decimal":"🍄","name":"mushroom","shortname":":mushroom:","category":"nature","emoji_order":"1471","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","drugs"]},"peanuts":{"unicode":"1f95c","unicode_alt":"","code_decimal":"🥜","name":"peanuts","shortname":":peanuts:","category":"food","emoji_order":"1472","aliases":[":shelled_peanut:"],"aliases_ascii":[],"keywords":[]},"chestnut":{"unicode":"1f330","unicode_alt":"","code_decimal":"🌰","name":"chestnut","shortname":":chestnut:","category":"nature","emoji_order":"1473","aliases":[],"aliases_ascii":[],"keywords":["nature","plant"]},"bread":{"unicode":"1f35e","unicode_alt":"","code_decimal":"🍞","name":"bread","shortname":":bread:","category":"food","emoji_order":"1474","aliases":[],"aliases_ascii":[],"keywords":["food"]},"croissant":{"unicode":"1f950","unicode_alt":"","code_decimal":"🥐","name":"croissant","shortname":":croissant:","category":"food","emoji_order":"1475","aliases":[],"aliases_ascii":[],"keywords":[]},"french_bread":{"unicode":"1f956","unicode_alt":"","code_decimal":"🥖","name":"baguette bread","shortname":":french_bread:","category":"food","emoji_order":"1476","aliases":[":baguette_bread:"],"aliases_ascii":[],"keywords":[]},"pancakes":{"unicode":"1f95e","unicode_alt":"","code_decimal":"🥞","name":"pancakes","shortname":":pancakes:","category":"food","emoji_order":"1477","aliases":[],"aliases_ascii":[],"keywords":[]},"cheese":{"unicode":"1f9c0","unicode_alt":"","code_decimal":"🧀","name":"cheese wedge","shortname":":cheese:","category":"food","emoji_order":"1478","aliases":[":cheese_wedge:"],"aliases_ascii":[],"keywords":["food"]},"meat_on_bone":{"unicode":"1f356","unicode_alt":"","code_decimal":"🍖","name":"meat on bone","shortname":":meat_on_bone:","category":"food","emoji_order":"1479","aliases":[],"aliases_ascii":[],"keywords":["food"]},"poultry_leg":{"unicode":"1f357","unicode_alt":"","code_decimal":"🍗","name":"poultry leg","shortname":":poultry_leg:","category":"food","emoji_order":"1480","aliases":[],"aliases_ascii":[],"keywords":["food","holidays"]},"bacon":{"unicode":"1f953","unicode_alt":"","code_decimal":"🥓","name":"bacon","shortname":":bacon:","category":"food","emoji_order":"1481","aliases":[],"aliases_ascii":[],"keywords":["pig"]},"hamburger":{"unicode":"1f354","unicode_alt":"","code_decimal":"🍔","name":"hamburger","shortname":":hamburger:","category":"food","emoji_order":"1482","aliases":[],"aliases_ascii":[],"keywords":["america","food"]},"fries":{"unicode":"1f35f","unicode_alt":"","code_decimal":"🍟","name":"french fries","shortname":":fries:","category":"food","emoji_order":"1483","aliases":[],"aliases_ascii":[],"keywords":["america","food"]},"pizza":{"unicode":"1f355","unicode_alt":"","code_decimal":"🍕","name":"slice of pizza","shortname":":pizza:","category":"food","emoji_order":"1484","aliases":[],"aliases_ascii":[],"keywords":["italian","food","boys night"]},"hotdog":{"unicode":"1f32d","unicode_alt":"","code_decimal":"🌭","name":"hot dog","shortname":":hotdog:","category":"food","emoji_order":"1485","aliases":[":hot_dog:"],"aliases_ascii":[],"keywords":["america","food"]},"taco":{"unicode":"1f32e","unicode_alt":"","code_decimal":"🌮","name":"taco","shortname":":taco:","category":"food","emoji_order":"1486","aliases":[],"aliases_ascii":[],"keywords":["food","mexican","vagina"]},"burrito":{"unicode":"1f32f","unicode_alt":"","code_decimal":"🌯","name":"burrito","shortname":":burrito:","category":"food","emoji_order":"1487","aliases":[],"aliases_ascii":[],"keywords":["food","mexican"]},"stuffed_flatbread":{"unicode":"1f959","unicode_alt":"","code_decimal":"🥙","name":"stuffed flatbread","shortname":":stuffed_flatbread:","category":"food","emoji_order":"1488","aliases":[":stuffed_pita:"],"aliases_ascii":[],"keywords":[]},"egg":{"unicode":"1f95a","unicode_alt":"","code_decimal":"🥚","name":"egg","shortname":":egg:","category":"food","emoji_order":"1489","aliases":[],"aliases_ascii":[],"keywords":[]},"cooking":{"unicode":"1f373","unicode_alt":"","code_decimal":"🍳","name":"cooking","shortname":":cooking:","category":"food","emoji_order":"1490","aliases":[],"aliases_ascii":[],"keywords":["food"]},"shallow_pan_of_food":{"unicode":"1f958","unicode_alt":"","code_decimal":"🥘","name":"shallow pan of food","shortname":":shallow_pan_of_food:","category":"food","emoji_order":"1491","aliases":[":paella:"],"aliases_ascii":[],"keywords":["pan of food"]},"stew":{"unicode":"1f372","unicode_alt":"","code_decimal":"🍲","name":"pot of food","shortname":":stew:","category":"food","emoji_order":"1492","aliases":[],"aliases_ascii":[],"keywords":["food","steam"]},"salad":{"unicode":"1f957","unicode_alt":"","code_decimal":"🥗","name":"green salad","shortname":":salad:","category":"food","emoji_order":"1493","aliases":[":green_salad:"],"aliases_ascii":[],"keywords":[]},"popcorn":{"unicode":"1f37f","unicode_alt":"","code_decimal":"🍿","name":"popcorn","shortname":":popcorn:","category":"food","emoji_order":"1494","aliases":[],"aliases_ascii":[],"keywords":["food","parties"]},"bento":{"unicode":"1f371","unicode_alt":"","code_decimal":"🍱","name":"bento box","shortname":":bento:","category":"food","emoji_order":"1495","aliases":[],"aliases_ascii":[],"keywords":["object","sushi","japan","food"]},"rice_cracker":{"unicode":"1f358","unicode_alt":"","code_decimal":"🍘","name":"rice cracker","shortname":":rice_cracker:","category":"food","emoji_order":"1496","aliases":[],"aliases_ascii":[],"keywords":["sushi","food"]},"rice_ball":{"unicode":"1f359","unicode_alt":"","code_decimal":"🍙","name":"rice ball","shortname":":rice_ball:","category":"food","emoji_order":"1497","aliases":[],"aliases_ascii":[],"keywords":["sushi","japan","food"]},"rice":{"unicode":"1f35a","unicode_alt":"","code_decimal":"🍚","name":"cooked rice","shortname":":rice:","category":"food","emoji_order":"1498","aliases":[],"aliases_ascii":[],"keywords":["sushi","japan","food"]},"curry":{"unicode":"1f35b","unicode_alt":"","code_decimal":"🍛","name":"curry and rice","shortname":":curry:","category":"food","emoji_order":"1499","aliases":[],"aliases_ascii":[],"keywords":["food"]},"ramen":{"unicode":"1f35c","unicode_alt":"","code_decimal":"🍜","name":"steaming bowl","shortname":":ramen:","category":"food","emoji_order":"1500","aliases":[],"aliases_ascii":[],"keywords":["noodles","ramen","japan","food"]},"spaghetti":{"unicode":"1f35d","unicode_alt":"","code_decimal":"🍝","name":"spaghetti","shortname":":spaghetti:","category":"food","emoji_order":"1501","aliases":[],"aliases_ascii":[],"keywords":["noodles","pasta","italian","food"]},"sweet_potato":{"unicode":"1f360","unicode_alt":"","code_decimal":"🍠","name":"roasted sweet potato","shortname":":sweet_potato:","category":"food","emoji_order":"1502","aliases":[],"aliases_ascii":[],"keywords":["vegetables","food"]},"oden":{"unicode":"1f362","unicode_alt":"","code_decimal":"🍢","name":"oden","shortname":":oden:","category":"food","emoji_order":"1503","aliases":[],"aliases_ascii":[],"keywords":["food"]},"sushi":{"unicode":"1f363","unicode_alt":"","code_decimal":"🍣","name":"sushi","shortname":":sushi:","category":"food","emoji_order":"1504","aliases":[],"aliases_ascii":[],"keywords":["sushi","japan","food"]},"fried_shrimp":{"unicode":"1f364","unicode_alt":"","code_decimal":"🍤","name":"fried shrimp","shortname":":fried_shrimp:","category":"food","emoji_order":"1505","aliases":[],"aliases_ascii":[],"keywords":["food"]},"fish_cake":{"unicode":"1f365","unicode_alt":"","code_decimal":"🍥","name":"fish cake with swirl design","shortname":":fish_cake:","category":"food","emoji_order":"1506","aliases":[],"aliases_ascii":[],"keywords":["sushi","food"]},"dango":{"unicode":"1f361","unicode_alt":"","code_decimal":"🍡","name":"dango","shortname":":dango:","category":"food","emoji_order":"1507","aliases":[],"aliases_ascii":[],"keywords":["food"]},"icecream":{"unicode":"1f366","unicode_alt":"","code_decimal":"🍦","name":"soft ice cream","shortname":":icecream:","category":"food","emoji_order":"1508","aliases":[],"aliases_ascii":[],"keywords":["food"]},"shaved_ice":{"unicode":"1f367","unicode_alt":"","code_decimal":"🍧","name":"shaved ice","shortname":":shaved_ice:","category":"food","emoji_order":"1509","aliases":[],"aliases_ascii":[],"keywords":["food"]},"ice_cream":{"unicode":"1f368","unicode_alt":"","code_decimal":"🍨","name":"ice cream","shortname":":ice_cream:","category":"food","emoji_order":"1510","aliases":[],"aliases_ascii":[],"keywords":["food"]},"doughnut":{"unicode":"1f369","unicode_alt":"","code_decimal":"🍩","name":"doughnut","shortname":":doughnut:","category":"food","emoji_order":"1511","aliases":[],"aliases_ascii":[],"keywords":["food"]},"cookie":{"unicode":"1f36a","unicode_alt":"","code_decimal":"🍪","name":"cookie","shortname":":cookie:","category":"food","emoji_order":"1512","aliases":[],"aliases_ascii":[],"keywords":["food","vagina"]},"birthday":{"unicode":"1f382","unicode_alt":"","code_decimal":"🎂","name":"birthday cake","shortname":":birthday:","category":"food","emoji_order":"1513","aliases":[],"aliases_ascii":[],"keywords":["birthday","food","parties"]},"cake":{"unicode":"1f370","unicode_alt":"","code_decimal":"🍰","name":"shortcake","shortname":":cake:","category":"food","emoji_order":"1514","aliases":[],"aliases_ascii":[],"keywords":["food"]},"chocolate_bar":{"unicode":"1f36b","unicode_alt":"","code_decimal":"🍫","name":"chocolate bar","shortname":":chocolate_bar:","category":"food","emoji_order":"1515","aliases":[],"aliases_ascii":[],"keywords":["food","halloween"]},"candy":{"unicode":"1f36c","unicode_alt":"","code_decimal":"🍬","name":"candy","shortname":":candy:","category":"food","emoji_order":"1516","aliases":[],"aliases_ascii":[],"keywords":["food","halloween"]},"lollipop":{"unicode":"1f36d","unicode_alt":"","code_decimal":"🍭","name":"lollipop","shortname":":lollipop:","category":"food","emoji_order":"1517","aliases":[],"aliases_ascii":[],"keywords":["food","halloween"]},"custard":{"unicode":"1f36e","unicode_alt":"","code_decimal":"🍮","name":"custard","shortname":":custard:","category":"food","emoji_order":"1518","aliases":[":pudding:",":flan:"],"aliases_ascii":[],"keywords":["food"]},"honey_pot":{"unicode":"1f36f","unicode_alt":"","code_decimal":"🍯","name":"honey pot","shortname":":honey_pot:","category":"food","emoji_order":"1519","aliases":[],"aliases_ascii":[],"keywords":["food","vagina"]},"baby_bottle":{"unicode":"1f37c","unicode_alt":"","code_decimal":"🍼","name":"baby bottle","shortname":":baby_bottle:","category":"food","emoji_order":"1520","aliases":[],"aliases_ascii":[],"keywords":["drink","object","food","baby"]},"milk":{"unicode":"1f95b","unicode_alt":"","code_decimal":"🥛","name":"glass of milk","shortname":":milk:","category":"food","emoji_order":"1521","aliases":[":glass_of_milk:"],"aliases_ascii":[],"keywords":[]},"coffee":{"unicode":"2615","unicode_alt":"2615-fe0f","code_decimal":"☕","name":"hot beverage","shortname":":coffee:","category":"food","emoji_order":"1522","aliases":[],"aliases_ascii":[],"keywords":["drink","caffeine","steam","morning"]},"tea":{"unicode":"1f375","unicode_alt":"","code_decimal":"🍵","name":"teacup without handle","shortname":":tea:","category":"food","emoji_order":"1523","aliases":[],"aliases_ascii":[],"keywords":["drink","japan","caffeine","steam","morning"]},"sake":{"unicode":"1f376","unicode_alt":"","code_decimal":"🍶","name":"sake bottle and cup","shortname":":sake:","category":"food","emoji_order":"1524","aliases":[],"aliases_ascii":[],"keywords":["drink","japan","sake","alcohol","girls night"]},"champagne":{"unicode":"1f37e","unicode_alt":"","code_decimal":"🍾","name":"bottle with popping cork","shortname":":champagne:","category":"food","emoji_order":"1525","aliases":[":bottle_with_popping_cork:"],"aliases_ascii":[],"keywords":["drink","cheers","alcohol","parties"]},"wine_glass":{"unicode":"1f377","unicode_alt":"","code_decimal":"🍷","name":"wine glass","shortname":":wine_glass:","category":"food","emoji_order":"1526","aliases":[],"aliases_ascii":[],"keywords":["drink","italian","alcohol","girls night","parties"]},"cocktail":{"unicode":"1f378","unicode_alt":"","code_decimal":"🍸","name":"cocktail glass","shortname":":cocktail:","category":"food","emoji_order":"1527","aliases":[],"aliases_ascii":[],"keywords":["drink","cocktail","alcohol","girls night","parties"]},"tropical_drink":{"unicode":"1f379","unicode_alt":"","code_decimal":"🍹","name":"tropical drink","shortname":":tropical_drink:","category":"food","emoji_order":"1528","aliases":[],"aliases_ascii":[],"keywords":["drink","cocktail","tropical","alcohol"]},"beer":{"unicode":"1f37a","unicode_alt":"","code_decimal":"🍺","name":"beer mug","shortname":":beer:","category":"food","emoji_order":"1529","aliases":[],"aliases_ascii":[],"keywords":["drink","beer","alcohol","parties"]},"beers":{"unicode":"1f37b","unicode_alt":"","code_decimal":"🍻","name":"clinking beer mugs","shortname":":beers:","category":"food","emoji_order":"1530","aliases":[],"aliases_ascii":[],"keywords":["drink","cheers","beer","alcohol","thank you","boys night","parties"]},"champagne_glass":{"unicode":"1f942","unicode_alt":"","code_decimal":"🥂","name":"clinking glasses","shortname":":champagne_glass:","category":"food","emoji_order":"1531","aliases":[":clinking_glass:"],"aliases_ascii":[],"keywords":[]},"tumbler_glass":{"unicode":"1f943","unicode_alt":"","code_decimal":"🥃","name":"tumbler glass","shortname":":tumbler_glass:","category":"food","emoji_order":"1532","aliases":[":whisky:"],"aliases_ascii":[],"keywords":["booze"]},"fork_knife_plate":{"unicode":"1f37d","unicode_alt":"1f37d-fe0f","code_decimal":"🍽","name":"fork and knife with plate","shortname":":fork_knife_plate:","category":"food","emoji_order":"1533","aliases":[":fork_and_knife_with_plate:"],"aliases_ascii":[],"keywords":["object","food"]},"fork_and_knife":{"unicode":"1f374","unicode_alt":"","code_decimal":"🍴","name":"fork and knife","shortname":":fork_and_knife:","category":"food","emoji_order":"1534","aliases":[],"aliases_ascii":[],"keywords":["object","weapon","food"]},"spoon":{"unicode":"1f944","unicode_alt":"","code_decimal":"🥄","name":"spoon","shortname":":spoon:","category":"food","emoji_order":"1535","aliases":[],"aliases_ascii":[],"keywords":[]},"knife":{"unicode":"1f52a","unicode_alt":"","code_decimal":"🔪","name":"hocho","shortname":":knife:","category":"objects","emoji_order":"1536","aliases":[],"aliases_ascii":[],"keywords":["object","weapon"]},"amphora":{"unicode":"1f3fa","unicode_alt":"","code_decimal":"🏺","name":"amphora","shortname":":amphora:","category":"objects","emoji_order":"1537","aliases":[],"aliases_ascii":[],"keywords":["object"]},"earth_africa":{"unicode":"1f30d","unicode_alt":"","code_decimal":"🌍","name":"earth globe europe-africa","shortname":":earth_africa:","category":"nature","emoji_order":"1538","aliases":[],"aliases_ascii":[],"keywords":["map","vacation","globe"]},"earth_americas":{"unicode":"1f30e","unicode_alt":"","code_decimal":"🌎","name":"earth globe americas","shortname":":earth_americas:","category":"nature","emoji_order":"1539","aliases":[],"aliases_ascii":[],"keywords":["map","vacation","globe"]},"earth_asia":{"unicode":"1f30f","unicode_alt":"","code_decimal":"🌏","name":"earth globe asia-australia","shortname":":earth_asia:","category":"nature","emoji_order":"1540","aliases":[],"aliases_ascii":[],"keywords":["map","vacation","globe"]},"globe_with_meridians":{"unicode":"1f310","unicode_alt":"","code_decimal":"🌐","name":"globe with meridians","shortname":":globe_with_meridians:","category":"symbols","emoji_order":"1541","aliases":[],"aliases_ascii":[],"keywords":["symbol","globe"]},"map":{"unicode":"1f5fa","unicode_alt":"1f5fa-fe0f","code_decimal":"🗺","name":"world map","shortname":":map:","category":"objects","emoji_order":"1542","aliases":[":world_map:"],"aliases_ascii":[],"keywords":["travel","map","vacation"]},"japan":{"unicode":"1f5fe","unicode_alt":"","code_decimal":"🗾","name":"silhouette of japan","shortname":":japan:","category":"travel","emoji_order":"1543","aliases":[],"aliases_ascii":[],"keywords":["places","travel","map","vacation","tropical"]},"mountain_snow":{"unicode":"1f3d4","unicode_alt":"1f3d4-fe0f","code_decimal":"🏔","name":"snow capped mountain","shortname":":mountain_snow:","category":"travel","emoji_order":"1544","aliases":[":snow_capped_mountain:"],"aliases_ascii":[],"keywords":["places","travel","vacation","cold","camp"]},"mountain":{"unicode":"26f0","unicode_alt":"26f0-fe0f","code_decimal":"⛰","name":"mountain","shortname":":mountain:","category":"travel","emoji_order":"1545","aliases":[],"aliases_ascii":[],"keywords":["places","travel","vacation","camp"]},"volcano":{"unicode":"1f30b","unicode_alt":"","code_decimal":"🌋","name":"volcano","shortname":":volcano:","category":"travel","emoji_order":"1546","aliases":[],"aliases_ascii":[],"keywords":["places","tropical"]},"mount_fuji":{"unicode":"1f5fb","unicode_alt":"","code_decimal":"🗻","name":"mount fuji","shortname":":mount_fuji:","category":"travel","emoji_order":"1547","aliases":[],"aliases_ascii":[],"keywords":["places","travel","vacation","cold","camp"]},"camping":{"unicode":"1f3d5","unicode_alt":"1f3d5-fe0f","code_decimal":"🏕","name":"camping","shortname":":camping:","category":"travel","emoji_order":"1548","aliases":[],"aliases_ascii":[],"keywords":["places","travel","vacation","camp"]},"beach":{"unicode":"1f3d6","unicode_alt":"1f3d6-fe0f","code_decimal":"🏖","name":"beach with umbrella","shortname":":beach:","category":"travel","emoji_order":"1549","aliases":[":beach_with_umbrella:"],"aliases_ascii":[],"keywords":["places","travel","vacation","tropical","beach","swim"]},"desert":{"unicode":"1f3dc","unicode_alt":"1f3dc-fe0f","code_decimal":"🏜","name":"desert","shortname":":desert:","category":"travel","emoji_order":"1550","aliases":[],"aliases_ascii":[],"keywords":["places","travel","vacation","hot"]},"island":{"unicode":"1f3dd","unicode_alt":"1f3dd-fe0f","code_decimal":"🏝","name":"desert island","shortname":":island:","category":"travel","emoji_order":"1551","aliases":[":desert_island:"],"aliases_ascii":[],"keywords":["places","travel","vacation","tropical","beach","swim"]},"park":{"unicode":"1f3de","unicode_alt":"1f3de-fe0f","code_decimal":"🏞","name":"national park","shortname":":park:","category":"travel","emoji_order":"1552","aliases":[":national_park:"],"aliases_ascii":[],"keywords":["travel","vacation","park","camp"]},"stadium":{"unicode":"1f3df","unicode_alt":"1f3df-fe0f","code_decimal":"🏟","name":"stadium","shortname":":stadium:","category":"travel","emoji_order":"1553","aliases":[],"aliases_ascii":[],"keywords":["places","building","travel","vacation","boys night"]},"classical_building":{"unicode":"1f3db","unicode_alt":"1f3db-fe0f","code_decimal":"🏛","name":"classical building","shortname":":classical_building:","category":"travel","emoji_order":"1554","aliases":[],"aliases_ascii":[],"keywords":["places","building","travel","vacation"]},"construction_site":{"unicode":"1f3d7","unicode_alt":"1f3d7-fe0f","code_decimal":"🏗","name":"building construction","shortname":":construction_site:","category":"travel","emoji_order":"1555","aliases":[":building_construction:"],"aliases_ascii":[],"keywords":["building","crane"]},"homes":{"unicode":"1f3d8","unicode_alt":"1f3d8-fe0f","code_decimal":"🏘","name":"house buildings","shortname":":homes:","category":"travel","emoji_order":"1556","aliases":[":house_buildings:"],"aliases_ascii":[],"keywords":["places","building","house"]},"cityscape":{"unicode":"1f3d9","unicode_alt":"1f3d9-fe0f","code_decimal":"🏙","name":"cityscape","shortname":":cityscape:","category":"travel","emoji_order":"1557","aliases":[],"aliases_ascii":[],"keywords":["places","building","vacation"]},"house_abandoned":{"unicode":"1f3da","unicode_alt":"1f3da-fe0f","code_decimal":"🏚","name":"derelict house building","shortname":":house_abandoned:","category":"travel","emoji_order":"1558","aliases":[":derelict_house_building:"],"aliases_ascii":[],"keywords":["places","building","house"]},"house":{"unicode":"1f3e0","unicode_alt":"","code_decimal":"🏠","name":"house building","shortname":":house:","category":"travel","emoji_order":"1559","aliases":[],"aliases_ascii":[],"keywords":["places","building","house"]},"house_with_garden":{"unicode":"1f3e1","unicode_alt":"","code_decimal":"🏡","name":"house with garden","shortname":":house_with_garden:","category":"travel","emoji_order":"1560","aliases":[],"aliases_ascii":[],"keywords":["places","building","house"]},"office":{"unicode":"1f3e2","unicode_alt":"","code_decimal":"🏢","name":"office building","shortname":":office:","category":"travel","emoji_order":"1561","aliases":[],"aliases_ascii":[],"keywords":["places","building","work"]},"post_office":{"unicode":"1f3e3","unicode_alt":"","code_decimal":"🏣","name":"japanese post office","shortname":":post_office:","category":"travel","emoji_order":"1562","aliases":[],"aliases_ascii":[],"keywords":["places","building","post office"]},"european_post_office":{"unicode":"1f3e4","unicode_alt":"","code_decimal":"🏤","name":"european post office","shortname":":european_post_office:","category":"travel","emoji_order":"1563","aliases":[],"aliases_ascii":[],"keywords":["places","building","post office"]},"hospital":{"unicode":"1f3e5","unicode_alt":"","code_decimal":"🏥","name":"hospital","shortname":":hospital:","category":"travel","emoji_order":"1564","aliases":[],"aliases_ascii":[],"keywords":["places","building","health","911"]},"bank":{"unicode":"1f3e6","unicode_alt":"","code_decimal":"🏦","name":"bank","shortname":":bank:","category":"travel","emoji_order":"1565","aliases":[],"aliases_ascii":[],"keywords":["places","building"]},"hotel":{"unicode":"1f3e8","unicode_alt":"","code_decimal":"🏨","name":"hotel","shortname":":hotel:","category":"travel","emoji_order":"1566","aliases":[],"aliases_ascii":[],"keywords":["places","building","vacation"]},"love_hotel":{"unicode":"1f3e9","unicode_alt":"","code_decimal":"🏩","name":"love hotel","shortname":":love_hotel:","category":"travel","emoji_order":"1567","aliases":[],"aliases_ascii":[],"keywords":["places","building","love"]},"convenience_store":{"unicode":"1f3ea","unicode_alt":"","code_decimal":"🏪","name":"convenience store","shortname":":convenience_store:","category":"travel","emoji_order":"1568","aliases":[],"aliases_ascii":[],"keywords":["places","building"]},"school":{"unicode":"1f3eb","unicode_alt":"","code_decimal":"🏫","name":"school","shortname":":school:","category":"travel","emoji_order":"1569","aliases":[],"aliases_ascii":[],"keywords":["places","building"]},"department_store":{"unicode":"1f3ec","unicode_alt":"","code_decimal":"🏬","name":"department store","shortname":":department_store:","category":"travel","emoji_order":"1570","aliases":[],"aliases_ascii":[],"keywords":["places","building"]},"factory":{"unicode":"1f3ed","unicode_alt":"","code_decimal":"🏭","name":"factory","shortname":":factory:","category":"travel","emoji_order":"1571","aliases":[],"aliases_ascii":[],"keywords":["places","building","travel","steam"]},"japanese_castle":{"unicode":"1f3ef","unicode_alt":"","code_decimal":"🏯","name":"japanese castle","shortname":":japanese_castle:","category":"travel","emoji_order":"1572","aliases":[],"aliases_ascii":[],"keywords":["places","building","travel","vacation"]},"european_castle":{"unicode":"1f3f0","unicode_alt":"","code_decimal":"🏰","name":"european castle","shortname":":european_castle:","category":"travel","emoji_order":"1573","aliases":[],"aliases_ascii":[],"keywords":["places","building","travel","vacation"]},"wedding":{"unicode":"1f492","unicode_alt":"","code_decimal":"💒","name":"wedding","shortname":":wedding:","category":"travel","emoji_order":"1574","aliases":[],"aliases_ascii":[],"keywords":["places","wedding","building","love","parties"]},"tokyo_tower":{"unicode":"1f5fc","unicode_alt":"","code_decimal":"🗼","name":"tokyo tower","shortname":":tokyo_tower:","category":"travel","emoji_order":"1575","aliases":[],"aliases_ascii":[],"keywords":["places","travel","vacation","eiffel tower"]},"statue_of_liberty":{"unicode":"1f5fd","unicode_alt":"","code_decimal":"🗽","name":"statue of liberty","shortname":":statue_of_liberty:","category":"travel","emoji_order":"1576","aliases":[],"aliases_ascii":[],"keywords":["places","america","travel","vacation","statue of liberty","free speech"]},"church":{"unicode":"26ea","unicode_alt":"26ea-fe0f","code_decimal":"⛪","name":"church","shortname":":church:","category":"travel","emoji_order":"1577","aliases":[],"aliases_ascii":[],"keywords":["places","wedding","religion","building","condolence"]},"mosque":{"unicode":"1f54c","unicode_alt":"","code_decimal":"🕌","name":"mosque","shortname":":mosque:","category":"travel","emoji_order":"1578","aliases":[],"aliases_ascii":[],"keywords":["places","religion","building","vacation","condolence"]},"synagogue":{"unicode":"1f54d","unicode_alt":"","code_decimal":"🕍","name":"synagogue","shortname":":synagogue:","category":"travel","emoji_order":"1579","aliases":[],"aliases_ascii":[],"keywords":["places","religion","building","travel","vacation","condolence"]},"shinto_shrine":{"unicode":"26e9","unicode_alt":"26e9-fe0f","code_decimal":"⛩","name":"shinto shrine","shortname":":shinto_shrine:","category":"travel","emoji_order":"1580","aliases":[],"aliases_ascii":[],"keywords":["places","building","travel","vacation"]},"kaaba":{"unicode":"1f54b","unicode_alt":"","code_decimal":"🕋","name":"kaaba","shortname":":kaaba:","category":"travel","emoji_order":"1581","aliases":[],"aliases_ascii":[],"keywords":["places","religion","building","condolence"]},"fountain":{"unicode":"26f2","unicode_alt":"26f2-fe0f","code_decimal":"⛲","name":"fountain","shortname":":fountain:","category":"travel","emoji_order":"1582","aliases":[],"aliases_ascii":[],"keywords":["travel","vacation"]},"tent":{"unicode":"26fa","unicode_alt":"26fa-fe0f","code_decimal":"⛺","name":"tent","shortname":":tent:","category":"travel","emoji_order":"1583","aliases":[],"aliases_ascii":[],"keywords":["places","travel","vacation","camp"]},"foggy":{"unicode":"1f301","unicode_alt":"","code_decimal":"🌁","name":"foggy","shortname":":foggy:","category":"travel","emoji_order":"1584","aliases":[],"aliases_ascii":[],"keywords":["places","building","sky","travel","vacation"]},"night_with_stars":{"unicode":"1f303","unicode_alt":"","code_decimal":"🌃","name":"night with stars","shortname":":night_with_stars:","category":"travel","emoji_order":"1585","aliases":[],"aliases_ascii":[],"keywords":["places","building","sky","vacation","goodnight"]},"sunrise_over_mountains":{"unicode":"1f304","unicode_alt":"","code_decimal":"🌄","name":"sunrise over mountains","shortname":":sunrise_over_mountains:","category":"travel","emoji_order":"1586","aliases":[],"aliases_ascii":[],"keywords":["places","sky","travel","vacation","day","sun","camp","morning"]},"sunrise":{"unicode":"1f305","unicode_alt":"","code_decimal":"🌅","name":"sunrise","shortname":":sunrise:","category":"travel","emoji_order":"1587","aliases":[],"aliases_ascii":[],"keywords":["places","sky","travel","vacation","tropical","day","sun","hump day","morning"]},"city_dusk":{"unicode":"1f306","unicode_alt":"","code_decimal":"🌆","name":"cityscape at dusk","shortname":":city_dusk:","category":"travel","emoji_order":"1588","aliases":[],"aliases_ascii":[],"keywords":["places","building"]},"city_sunset":{"unicode":"1f307","unicode_alt":"","code_decimal":"🌇","name":"sunset over buildings","shortname":":city_sunset:","category":"travel","emoji_order":"1589","aliases":[":city_sunrise:"],"aliases_ascii":[],"keywords":["places","building","sky","vacation"]},"bridge_at_night":{"unicode":"1f309","unicode_alt":"","code_decimal":"🌉","name":"bridge at night","shortname":":bridge_at_night:","category":"travel","emoji_order":"1590","aliases":[],"aliases_ascii":[],"keywords":["places","travel","vacation","goodnight"]},"hotsprings":{"unicode":"2668","unicode_alt":"2668-fe0f","code_decimal":"♨","name":"hot springs","shortname":":hotsprings:","category":"symbols","emoji_order":"1591","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"milky_way":{"unicode":"1f30c","unicode_alt":"","code_decimal":"🌌","name":"milky way","shortname":":milky_way:","category":"travel","emoji_order":"1592","aliases":[],"aliases_ascii":[],"keywords":["places","space","sky","travel","vacation"]},"carousel_horse":{"unicode":"1f3a0","unicode_alt":"","code_decimal":"🎠","name":"carousel horse","shortname":":carousel_horse:","category":"travel","emoji_order":"1593","aliases":[],"aliases_ascii":[],"keywords":["places","object","vacation","roller coaster","carousel"]},"ferris_wheel":{"unicode":"1f3a1","unicode_alt":"","code_decimal":"🎡","name":"ferris wheel","shortname":":ferris_wheel:","category":"travel","emoji_order":"1594","aliases":[],"aliases_ascii":[],"keywords":["places","vacation","ferris wheel"]},"roller_coaster":{"unicode":"1f3a2","unicode_alt":"","code_decimal":"🎢","name":"roller coaster","shortname":":roller_coaster:","category":"travel","emoji_order":"1595","aliases":[],"aliases_ascii":[],"keywords":["places","vacation","roller coaster"]},"barber":{"unicode":"1f488","unicode_alt":"","code_decimal":"💈","name":"barber pole","shortname":":barber:","category":"objects","emoji_order":"1596","aliases":[],"aliases_ascii":[],"keywords":["object"]},"circus_tent":{"unicode":"1f3aa","unicode_alt":"","code_decimal":"🎪","name":"circus tent","shortname":":circus_tent:","category":"activity","emoji_order":"1597","aliases":[],"aliases_ascii":[],"keywords":["circus tent"]},"performing_arts":{"unicode":"1f3ad","unicode_alt":"","code_decimal":"🎭","name":"performing arts","shortname":":performing_arts:","category":"activity","emoji_order":"1598","aliases":[],"aliases_ascii":[],"keywords":["theatre","movie"]},"frame_photo":{"unicode":"1f5bc","unicode_alt":"1f5bc-fe0f","code_decimal":"🖼","name":"frame with picture","shortname":":frame_photo:","category":"objects","emoji_order":"1599","aliases":[":frame_with_picture:"],"aliases_ascii":[],"keywords":["travel","vacation"]},"art":{"unicode":"1f3a8","unicode_alt":"","code_decimal":"🎨","name":"artist palette","shortname":":art:","category":"activity","emoji_order":"1600","aliases":[],"aliases_ascii":[],"keywords":[]},"slot_machine":{"unicode":"1f3b0","unicode_alt":"","code_decimal":"🎰","name":"slot machine","shortname":":slot_machine:","category":"activity","emoji_order":"1601","aliases":[],"aliases_ascii":[],"keywords":["game","boys night"]},"steam_locomotive":{"unicode":"1f682","unicode_alt":"","code_decimal":"🚂","name":"steam locomotive","shortname":":steam_locomotive:","category":"travel","emoji_order":"1602","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train","steam"]},"railway_car":{"unicode":"1f683","unicode_alt":"","code_decimal":"🚃","name":"railway car","shortname":":railway_car:","category":"travel","emoji_order":"1603","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"bullettrain_side":{"unicode":"1f684","unicode_alt":"","code_decimal":"🚄","name":"high-speed train","shortname":":bullettrain_side:","category":"travel","emoji_order":"1604","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"bullettrain_front":{"unicode":"1f685","unicode_alt":"","code_decimal":"🚅","name":"high-speed train with bullet nose","shortname":":bullettrain_front:","category":"travel","emoji_order":"1605","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"train2":{"unicode":"1f686","unicode_alt":"","code_decimal":"🚆","name":"train","shortname":":train2:","category":"travel","emoji_order":"1606","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"metro":{"unicode":"1f687","unicode_alt":"","code_decimal":"🚇","name":"metro","shortname":":metro:","category":"travel","emoji_order":"1607","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"light_rail":{"unicode":"1f688","unicode_alt":"","code_decimal":"🚈","name":"light rail","shortname":":light_rail:","category":"travel","emoji_order":"1608","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"station":{"unicode":"1f689","unicode_alt":"","code_decimal":"🚉","name":"station","shortname":":station:","category":"travel","emoji_order":"1609","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"tram":{"unicode":"1f68a","unicode_alt":"","code_decimal":"🚊","name":"tram","shortname":":tram:","category":"travel","emoji_order":"1610","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"monorail":{"unicode":"1f69d","unicode_alt":"","code_decimal":"🚝","name":"monorail","shortname":":monorail:","category":"travel","emoji_order":"1611","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train","vacation"]},"mountain_railway":{"unicode":"1f69e","unicode_alt":"","code_decimal":"🚞","name":"mountain railway","shortname":":mountain_railway:","category":"travel","emoji_order":"1612","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"train":{"unicode":"1f68b","unicode_alt":"","code_decimal":"🚋","name":"tram car","shortname":":train:","category":"travel","emoji_order":"1613","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"bus":{"unicode":"1f68c","unicode_alt":"","code_decimal":"🚌","name":"bus","shortname":":bus:","category":"travel","emoji_order":"1614","aliases":[],"aliases_ascii":[],"keywords":["transportation","bus","office"]},"oncoming_bus":{"unicode":"1f68d","unicode_alt":"","code_decimal":"🚍","name":"oncoming bus","shortname":":oncoming_bus:","category":"travel","emoji_order":"1615","aliases":[],"aliases_ascii":[],"keywords":["transportation","bus","travel"]},"trolleybus":{"unicode":"1f68e","unicode_alt":"","code_decimal":"🚎","name":"trolleybus","shortname":":trolleybus:","category":"travel","emoji_order":"1616","aliases":[],"aliases_ascii":[],"keywords":["transportation","bus","travel"]},"minibus":{"unicode":"1f690","unicode_alt":"","code_decimal":"🚐","name":"minibus","shortname":":minibus:","category":"travel","emoji_order":"1617","aliases":[],"aliases_ascii":[],"keywords":["transportation","bus"]},"ambulance":{"unicode":"1f691","unicode_alt":"","code_decimal":"🚑","name":"ambulance","shortname":":ambulance:","category":"travel","emoji_order":"1618","aliases":[],"aliases_ascii":[],"keywords":["transportation","911"]},"fire_engine":{"unicode":"1f692","unicode_alt":"","code_decimal":"🚒","name":"fire engine","shortname":":fire_engine:","category":"travel","emoji_order":"1619","aliases":[],"aliases_ascii":[],"keywords":["transportation","truck","911"]},"police_car":{"unicode":"1f693","unicode_alt":"","code_decimal":"🚓","name":"police car","shortname":":police_car:","category":"travel","emoji_order":"1620","aliases":[],"aliases_ascii":[],"keywords":["transportation","car","police","911"]},"oncoming_police_car":{"unicode":"1f694","unicode_alt":"","code_decimal":"🚔","name":"oncoming police car","shortname":":oncoming_police_car:","category":"travel","emoji_order":"1621","aliases":[],"aliases_ascii":[],"keywords":["transportation","car","police","911"]},"taxi":{"unicode":"1f695","unicode_alt":"","code_decimal":"🚕","name":"taxi","shortname":":taxi:","category":"travel","emoji_order":"1622","aliases":[],"aliases_ascii":[],"keywords":["transportation","car","travel"]},"oncoming_taxi":{"unicode":"1f696","unicode_alt":"","code_decimal":"🚖","name":"oncoming taxi","shortname":":oncoming_taxi:","category":"travel","emoji_order":"1623","aliases":[],"aliases_ascii":[],"keywords":["transportation","car","travel"]},"red_car":{"unicode":"1f697","unicode_alt":"","code_decimal":"🚗","name":"automobile","shortname":":red_car:","category":"travel","emoji_order":"1624","aliases":[],"aliases_ascii":[],"keywords":["transportation","car","travel"]},"oncoming_automobile":{"unicode":"1f698","unicode_alt":"","code_decimal":"🚘","name":"oncoming automobile","shortname":":oncoming_automobile:","category":"travel","emoji_order":"1625","aliases":[],"aliases_ascii":[],"keywords":["transportation","car","travel"]},"blue_car":{"unicode":"1f699","unicode_alt":"","code_decimal":"🚙","name":"recreational vehicle","shortname":":blue_car:","category":"travel","emoji_order":"1626","aliases":[],"aliases_ascii":[],"keywords":["transportation","car","travel"]},"truck":{"unicode":"1f69a","unicode_alt":"","code_decimal":"🚚","name":"delivery truck","shortname":":truck:","category":"travel","emoji_order":"1627","aliases":[],"aliases_ascii":[],"keywords":["transportation","truck"]},"articulated_lorry":{"unicode":"1f69b","unicode_alt":"","code_decimal":"🚛","name":"articulated lorry","shortname":":articulated_lorry:","category":"travel","emoji_order":"1628","aliases":[],"aliases_ascii":[],"keywords":["transportation","truck"]},"tractor":{"unicode":"1f69c","unicode_alt":"","code_decimal":"🚜","name":"tractor","shortname":":tractor:","category":"travel","emoji_order":"1629","aliases":[],"aliases_ascii":[],"keywords":["transportation"]},"bike":{"unicode":"1f6b2","unicode_alt":"","code_decimal":"🚲","name":"bicycle","shortname":":bike:","category":"travel","emoji_order":"1630","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","bike"]},"scooter":{"unicode":"1f6f4","unicode_alt":"","code_decimal":"🛴","name":"scooter","shortname":":scooter:","category":"travel","emoji_order":"1631","aliases":[],"aliases_ascii":[],"keywords":[]},"motor_scooter":{"unicode":"1f6f5","unicode_alt":"","code_decimal":"🛵","name":"motor scooter","shortname":":motor_scooter:","category":"travel","emoji_order":"1632","aliases":[":motorbike:"],"aliases_ascii":[],"keywords":["moped"]},"busstop":{"unicode":"1f68f","unicode_alt":"","code_decimal":"🚏","name":"bus stop","shortname":":busstop:","category":"travel","emoji_order":"1633","aliases":[],"aliases_ascii":[],"keywords":["object"]},"motorway":{"unicode":"1f6e3","unicode_alt":"1f6e3-fe0f","code_decimal":"🛣","name":"motorway","shortname":":motorway:","category":"travel","emoji_order":"1634","aliases":[],"aliases_ascii":[],"keywords":["travel","vacation","camp"]},"railway_track":{"unicode":"1f6e4","unicode_alt":"1f6e4-fe0f","code_decimal":"🛤","name":"railway track","shortname":":railway_track:","category":"travel","emoji_order":"1635","aliases":[":railroad_track:"],"aliases_ascii":[],"keywords":["travel","train","vacation"]},"fuelpump":{"unicode":"26fd","unicode_alt":"26fd-fe0f","code_decimal":"⛽","name":"fuel pump","shortname":":fuelpump:","category":"travel","emoji_order":"1636","aliases":[],"aliases_ascii":[],"keywords":["object","gas pump"]},"rotating_light":{"unicode":"1f6a8","unicode_alt":"","code_decimal":"🚨","name":"police cars revolving light","shortname":":rotating_light:","category":"travel","emoji_order":"1637","aliases":[],"aliases_ascii":[],"keywords":["transportation","object","police","911"]},"traffic_light":{"unicode":"1f6a5","unicode_alt":"","code_decimal":"🚥","name":"horizontal traffic light","shortname":":traffic_light:","category":"travel","emoji_order":"1638","aliases":[],"aliases_ascii":[],"keywords":["object","stop light"]},"vertical_traffic_light":{"unicode":"1f6a6","unicode_alt":"","code_decimal":"🚦","name":"vertical traffic light","shortname":":vertical_traffic_light:","category":"travel","emoji_order":"1639","aliases":[],"aliases_ascii":[],"keywords":["object","stop light"]},"construction":{"unicode":"1f6a7","unicode_alt":"","code_decimal":"🚧","name":"construction sign","shortname":":construction:","category":"travel","emoji_order":"1640","aliases":[],"aliases_ascii":[],"keywords":["object"]},"octagonal_sign":{"unicode":"1f6d1","unicode_alt":"","code_decimal":"🛑","name":"octagonal sign","shortname":":octagonal_sign:","category":"symbols","emoji_order":"1641","aliases":[":stop_sign:"],"aliases_ascii":[],"keywords":[]},"anchor":{"unicode":"2693","unicode_alt":"2693-fe0f","code_decimal":"⚓","name":"anchor","shortname":":anchor:","category":"travel","emoji_order":"1642","aliases":[],"aliases_ascii":[],"keywords":["object","travel","boat","vacation"]},"sailboat":{"unicode":"26f5","unicode_alt":"26f5-fe0f","code_decimal":"⛵","name":"sailboat","shortname":":sailboat:","category":"travel","emoji_order":"1643","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","boat","vacation"]},"canoe":{"unicode":"1f6f6","unicode_alt":"","code_decimal":"🛶","name":"canoe","shortname":":canoe:","category":"travel","emoji_order":"1644","aliases":[":kayak:"],"aliases_ascii":[],"keywords":[]},"speedboat":{"unicode":"1f6a4","unicode_alt":"","code_decimal":"🚤","name":"speedboat","shortname":":speedboat:","category":"travel","emoji_order":"1645","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","boat","vacation","tropical"]},"cruise_ship":{"unicode":"1f6f3","unicode_alt":"1f6f3-fe0f","code_decimal":"🛳","name":"passenger ship","shortname":":cruise_ship:","category":"travel","emoji_order":"1646","aliases":[":passenger_ship:"],"aliases_ascii":[],"keywords":["transportation","travel","boat","vacation"]},"ferry":{"unicode":"26f4","unicode_alt":"26f4-fe0f","code_decimal":"⛴","name":"ferry","shortname":":ferry:","category":"travel","emoji_order":"1647","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","boat","vacation"]},"motorboat":{"unicode":"1f6e5","unicode_alt":"1f6e5-fe0f","code_decimal":"🛥","name":"motorboat","shortname":":motorboat:","category":"travel","emoji_order":"1648","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","boat"]},"ship":{"unicode":"1f6a2","unicode_alt":"","code_decimal":"🚢","name":"ship","shortname":":ship:","category":"travel","emoji_order":"1649","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","boat","vacation"]},"airplane":{"unicode":"2708","unicode_alt":"2708-fe0f","code_decimal":"✈","name":"airplane","shortname":":airplane:","category":"travel","emoji_order":"1650","aliases":[],"aliases_ascii":[],"keywords":["transportation","plane","travel","vacation","fly"]},"airplane_small":{"unicode":"1f6e9","unicode_alt":"1f6e9-fe0f","code_decimal":"🛩","name":"small airplane","shortname":":airplane_small:","category":"travel","emoji_order":"1651","aliases":[":small_airplane:"],"aliases_ascii":[],"keywords":["transportation","plane","travel","vacation","fly"]},"airplane_departure":{"unicode":"1f6eb","unicode_alt":"","code_decimal":"🛫","name":"airplane departure","shortname":":airplane_departure:","category":"travel","emoji_order":"1652","aliases":[],"aliases_ascii":[],"keywords":["transportation","plane","travel","vacation","fly"]},"airplane_arriving":{"unicode":"1f6ec","unicode_alt":"","code_decimal":"🛬","name":"airplane arriving","shortname":":airplane_arriving:","category":"travel","emoji_order":"1653","aliases":[],"aliases_ascii":[],"keywords":["transportation","plane","travel","vacation","fly"]},"seat":{"unicode":"1f4ba","unicode_alt":"","code_decimal":"💺","name":"seat","shortname":":seat:","category":"travel","emoji_order":"1654","aliases":[],"aliases_ascii":[],"keywords":["transportation","object","travel","vacation"]},"helicopter":{"unicode":"1f681","unicode_alt":"","code_decimal":"🚁","name":"helicopter","shortname":":helicopter:","category":"travel","emoji_order":"1655","aliases":[],"aliases_ascii":[],"keywords":["transportation","plane","travel","fly"]},"suspension_railway":{"unicode":"1f69f","unicode_alt":"","code_decimal":"🚟","name":"suspension railway","shortname":":suspension_railway:","category":"travel","emoji_order":"1656","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"mountain_cableway":{"unicode":"1f6a0","unicode_alt":"","code_decimal":"🚠","name":"mountain cableway","shortname":":mountain_cableway:","category":"travel","emoji_order":"1657","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"aerial_tramway":{"unicode":"1f6a1","unicode_alt":"","code_decimal":"🚡","name":"aerial tramway","shortname":":aerial_tramway:","category":"travel","emoji_order":"1658","aliases":[],"aliases_ascii":[],"keywords":["transportation","travel","train"]},"rocket":{"unicode":"1f680","unicode_alt":"","code_decimal":"🚀","name":"rocket","shortname":":rocket:","category":"travel","emoji_order":"1659","aliases":[],"aliases_ascii":[],"keywords":["transportation","object","space","fly","blast"]},"satellite_orbital":{"unicode":"1f6f0","unicode_alt":"1f6f0-fe0f","code_decimal":"🛰","name":"satellite","shortname":":satellite_orbital:","category":"travel","emoji_order":"1660","aliases":[],"aliases_ascii":[],"keywords":["object"]},"bellhop":{"unicode":"1f6ce","unicode_alt":"1f6ce-fe0f","code_decimal":"🛎","name":"bellhop bell","shortname":":bellhop:","category":"objects","emoji_order":"1661","aliases":[":bellhop_bell:"],"aliases_ascii":[],"keywords":["object"]},"door":{"unicode":"1f6aa","unicode_alt":"","code_decimal":"🚪","name":"door","shortname":":door:","category":"objects","emoji_order":"1662","aliases":[],"aliases_ascii":[],"keywords":["object"]},"sleeping_accommodation":{"unicode":"1f6cc","unicode_alt":"","code_decimal":"🛌","name":"sleeping accommodation","shortname":":sleeping_accommodation:","category":"objects","emoji_order":"1663","aliases":[],"aliases_ascii":[],"keywords":["tired"]},"bed":{"unicode":"1f6cf","unicode_alt":"1f6cf-fe0f","code_decimal":"🛏","name":"bed","shortname":":bed:","category":"objects","emoji_order":"1669","aliases":[],"aliases_ascii":[],"keywords":["object","tired"]},"couch":{"unicode":"1f6cb","unicode_alt":"1f6cb-fe0f","code_decimal":"🛋","name":"couch and lamp","shortname":":couch:","category":"objects","emoji_order":"1670","aliases":[":couch_and_lamp:"],"aliases_ascii":[],"keywords":["object"]},"toilet":{"unicode":"1f6bd","unicode_alt":"","code_decimal":"🚽","name":"toilet","shortname":":toilet:","category":"objects","emoji_order":"1671","aliases":[],"aliases_ascii":[],"keywords":["object","bathroom"]},"shower":{"unicode":"1f6bf","unicode_alt":"","code_decimal":"🚿","name":"shower","shortname":":shower:","category":"objects","emoji_order":"1672","aliases":[],"aliases_ascii":[],"keywords":["object","bathroom"]},"bath":{"unicode":"1f6c0","unicode_alt":"","code_decimal":"🛀","name":"bath","shortname":":bath:","category":"activity","emoji_order":"1673","aliases":[],"aliases_ascii":[],"keywords":["bathroom","tired","diversity","steam"]},"bath_tone1":{"unicode":"1f6c0-1f3fb","unicode_alt":"","code_decimal":"🛀🏻","name":"bath tone 1","shortname":":bath_tone1:","category":"activity","emoji_order":"1674","aliases":[],"aliases_ascii":[],"keywords":[]},"bath_tone2":{"unicode":"1f6c0-1f3fc","unicode_alt":"","code_decimal":"🛀🏼","name":"bath tone 2","shortname":":bath_tone2:","category":"activity","emoji_order":"1675","aliases":[],"aliases_ascii":[],"keywords":[]},"bath_tone3":{"unicode":"1f6c0-1f3fd","unicode_alt":"","code_decimal":"🛀🏽","name":"bath tone 3","shortname":":bath_tone3:","category":"activity","emoji_order":"1676","aliases":[],"aliases_ascii":[],"keywords":[]},"bath_tone4":{"unicode":"1f6c0-1f3fe","unicode_alt":"","code_decimal":"🛀🏾","name":"bath tone 4","shortname":":bath_tone4:","category":"activity","emoji_order":"1677","aliases":[],"aliases_ascii":[],"keywords":[]},"bath_tone5":{"unicode":"1f6c0-1f3ff","unicode_alt":"","code_decimal":"🛀🏿","name":"bath tone 5","shortname":":bath_tone5:","category":"activity","emoji_order":"1678","aliases":[],"aliases_ascii":[],"keywords":[]},"bathtub":{"unicode":"1f6c1","unicode_alt":"","code_decimal":"🛁","name":"bathtub","shortname":":bathtub:","category":"objects","emoji_order":"1679","aliases":[],"aliases_ascii":[],"keywords":["object","bathroom","tired","steam"]},"hourglass":{"unicode":"231b","unicode_alt":"231b-fe0f","code_decimal":"⌛","name":"hourglass","shortname":":hourglass:","category":"objects","emoji_order":"1680","aliases":[],"aliases_ascii":[],"keywords":["object","time"]},"hourglass_flowing_sand":{"unicode":"23f3","unicode_alt":"","code_decimal":"⏳","name":"hourglass with flowing sand","shortname":":hourglass_flowing_sand:","category":"objects","emoji_order":"1681","aliases":[],"aliases_ascii":[],"keywords":["object","time"]},"watch":{"unicode":"231a","unicode_alt":"231a-fe0f","code_decimal":"⌚","name":"watch","shortname":":watch:","category":"objects","emoji_order":"1682","aliases":[],"aliases_ascii":[],"keywords":["electronics","time"]},"alarm_clock":{"unicode":"23f0","unicode_alt":"","code_decimal":"⏰","name":"alarm clock","shortname":":alarm_clock:","category":"objects","emoji_order":"1683","aliases":[],"aliases_ascii":[],"keywords":["object","time"]},"stopwatch":{"unicode":"23f1","unicode_alt":"23f1-fe0f","code_decimal":"⏱","name":"stopwatch","shortname":":stopwatch:","category":"objects","emoji_order":"1684","aliases":[],"aliases_ascii":[],"keywords":["electronics","time"]},"timer":{"unicode":"23f2","unicode_alt":"23f2-fe0f","code_decimal":"⏲","name":"timer clock","shortname":":timer:","category":"objects","emoji_order":"1685","aliases":[":timer_clock:"],"aliases_ascii":[],"keywords":["object","time"]},"clock":{"unicode":"1f570","unicode_alt":"1f570-fe0f","code_decimal":"🕰","name":"mantlepiece clock","shortname":":clock:","category":"objects","emoji_order":"1686","aliases":[":mantlepiece_clock:"],"aliases_ascii":[],"keywords":["object","time"]},"clock12":{"unicode":"1f55b","unicode_alt":"","code_decimal":"🕛","name":"clock face twelve oclock","shortname":":clock12:","category":"symbols","emoji_order":"1687","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock1230":{"unicode":"1f567","unicode_alt":"","code_decimal":"🕧","name":"clock face twelve-thirty","shortname":":clock1230:","category":"symbols","emoji_order":"1688","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock1":{"unicode":"1f550","unicode_alt":"","code_decimal":"🕐","name":"clock face one oclock","shortname":":clock1:","category":"symbols","emoji_order":"1689","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock130":{"unicode":"1f55c","unicode_alt":"","code_decimal":"🕜","name":"clock face one-thirty","shortname":":clock130:","category":"symbols","emoji_order":"1690","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock2":{"unicode":"1f551","unicode_alt":"","code_decimal":"🕑","name":"clock face two oclock","shortname":":clock2:","category":"symbols","emoji_order":"1691","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock230":{"unicode":"1f55d","unicode_alt":"","code_decimal":"🕝","name":"clock face two-thirty","shortname":":clock230:","category":"symbols","emoji_order":"1692","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock3":{"unicode":"1f552","unicode_alt":"","code_decimal":"🕒","name":"clock face three oclock","shortname":":clock3:","category":"symbols","emoji_order":"1693","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock330":{"unicode":"1f55e","unicode_alt":"","code_decimal":"🕞","name":"clock face three-thirty","shortname":":clock330:","category":"symbols","emoji_order":"1694","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock4":{"unicode":"1f553","unicode_alt":"","code_decimal":"🕓","name":"clock face four oclock","shortname":":clock4:","category":"symbols","emoji_order":"1695","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock430":{"unicode":"1f55f","unicode_alt":"","code_decimal":"🕟","name":"clock face four-thirty","shortname":":clock430:","category":"symbols","emoji_order":"1696","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock5":{"unicode":"1f554","unicode_alt":"","code_decimal":"🕔","name":"clock face five oclock","shortname":":clock5:","category":"symbols","emoji_order":"1697","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock530":{"unicode":"1f560","unicode_alt":"","code_decimal":"🕠","name":"clock face five-thirty","shortname":":clock530:","category":"symbols","emoji_order":"1698","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock6":{"unicode":"1f555","unicode_alt":"","code_decimal":"🕕","name":"clock face six oclock","shortname":":clock6:","category":"symbols","emoji_order":"1699","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock630":{"unicode":"1f561","unicode_alt":"","code_decimal":"🕡","name":"clock face six-thirty","shortname":":clock630:","category":"symbols","emoji_order":"1700","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock7":{"unicode":"1f556","unicode_alt":"","code_decimal":"🕖","name":"clock face seven oclock","shortname":":clock7:","category":"symbols","emoji_order":"1701","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock730":{"unicode":"1f562","unicode_alt":"","code_decimal":"🕢","name":"clock face seven-thirty","shortname":":clock730:","category":"symbols","emoji_order":"1702","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock8":{"unicode":"1f557","unicode_alt":"","code_decimal":"🕗","name":"clock face eight oclock","shortname":":clock8:","category":"symbols","emoji_order":"1703","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock830":{"unicode":"1f563","unicode_alt":"","code_decimal":"🕣","name":"clock face eight-thirty","shortname":":clock830:","category":"symbols","emoji_order":"1704","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock9":{"unicode":"1f558","unicode_alt":"","code_decimal":"🕘","name":"clock face nine oclock","shortname":":clock9:","category":"symbols","emoji_order":"1705","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock930":{"unicode":"1f564","unicode_alt":"","code_decimal":"🕤","name":"clock face nine-thirty","shortname":":clock930:","category":"symbols","emoji_order":"1706","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock10":{"unicode":"1f559","unicode_alt":"","code_decimal":"🕙","name":"clock face ten oclock","shortname":":clock10:","category":"symbols","emoji_order":"1707","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock1030":{"unicode":"1f565","unicode_alt":"","code_decimal":"🕥","name":"clock face ten-thirty","shortname":":clock1030:","category":"symbols","emoji_order":"1708","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock11":{"unicode":"1f55a","unicode_alt":"","code_decimal":"🕚","name":"clock face eleven oclock","shortname":":clock11:","category":"symbols","emoji_order":"1709","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"clock1130":{"unicode":"1f566","unicode_alt":"","code_decimal":"🕦","name":"clock face eleven-thirty","shortname":":clock1130:","category":"symbols","emoji_order":"1710","aliases":[],"aliases_ascii":[],"keywords":["symbol","time"]},"new_moon":{"unicode":"1f311","unicode_alt":"","code_decimal":"🌑","name":"new moon symbol","shortname":":new_moon:","category":"nature","emoji_order":"1711","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"waxing_crescent_moon":{"unicode":"1f312","unicode_alt":"","code_decimal":"🌒","name":"waxing crescent moon symbol","shortname":":waxing_crescent_moon:","category":"nature","emoji_order":"1712","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"first_quarter_moon":{"unicode":"1f313","unicode_alt":"","code_decimal":"🌓","name":"first quarter moon symbol","shortname":":first_quarter_moon:","category":"nature","emoji_order":"1713","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"waxing_gibbous_moon":{"unicode":"1f314","unicode_alt":"","code_decimal":"🌔","name":"waxing gibbous moon symbol","shortname":":waxing_gibbous_moon:","category":"nature","emoji_order":"1714","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"full_moon":{"unicode":"1f315","unicode_alt":"","code_decimal":"🌕","name":"full moon symbol","shortname":":full_moon:","category":"nature","emoji_order":"1715","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"waning_gibbous_moon":{"unicode":"1f316","unicode_alt":"","code_decimal":"🌖","name":"waning gibbous moon symbol","shortname":":waning_gibbous_moon:","category":"nature","emoji_order":"1716","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"last_quarter_moon":{"unicode":"1f317","unicode_alt":"","code_decimal":"🌗","name":"last quarter moon symbol","shortname":":last_quarter_moon:","category":"nature","emoji_order":"1717","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"waning_crescent_moon":{"unicode":"1f318","unicode_alt":"","code_decimal":"🌘","name":"waning crescent moon symbol","shortname":":waning_crescent_moon:","category":"nature","emoji_order":"1718","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"crescent_moon":{"unicode":"1f319","unicode_alt":"","code_decimal":"🌙","name":"crescent moon","shortname":":crescent_moon:","category":"nature","emoji_order":"1719","aliases":[],"aliases_ascii":[],"keywords":["space","sky","goodnight","moon"]},"new_moon_with_face":{"unicode":"1f31a","unicode_alt":"","code_decimal":"🌚","name":"new moon with face","shortname":":new_moon_with_face:","category":"nature","emoji_order":"1720","aliases":[],"aliases_ascii":[],"keywords":["space","sky","goodnight","moon"]},"first_quarter_moon_with_face":{"unicode":"1f31b","unicode_alt":"","code_decimal":"🌛","name":"first quarter moon with face","shortname":":first_quarter_moon_with_face:","category":"nature","emoji_order":"1721","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"last_quarter_moon_with_face":{"unicode":"1f31c","unicode_alt":"","code_decimal":"🌜","name":"last quarter moon with face","shortname":":last_quarter_moon_with_face:","category":"nature","emoji_order":"1722","aliases":[],"aliases_ascii":[],"keywords":["space","sky","moon"]},"thermometer":{"unicode":"1f321","unicode_alt":"1f321-fe0f","code_decimal":"🌡","name":"thermometer","shortname":":thermometer:","category":"objects","emoji_order":"1723","aliases":[],"aliases_ascii":[],"keywords":["object","science","health","hot"]},"sunny":{"unicode":"2600","unicode_alt":"2600-fe0f","code_decimal":"☀","name":"black sun with rays","shortname":":sunny:","category":"nature","emoji_order":"1724","aliases":[],"aliases_ascii":[],"keywords":["weather","sky","day","sun","hot","morning"]},"full_moon_with_face":{"unicode":"1f31d","unicode_alt":"","code_decimal":"🌝","name":"full moon with face","shortname":":full_moon_with_face:","category":"nature","emoji_order":"1725","aliases":[],"aliases_ascii":[],"keywords":["space","sky","goodnight","moon"]},"sun_with_face":{"unicode":"1f31e","unicode_alt":"","code_decimal":"🌞","name":"sun with face","shortname":":sun_with_face:","category":"nature","emoji_order":"1726","aliases":[],"aliases_ascii":[],"keywords":["sky","day","sun","hump day","morning"]},"star":{"unicode":"2b50","unicode_alt":"2b50-fe0f","code_decimal":"⭐","name":"white medium star","shortname":":star:","category":"nature","emoji_order":"1727","aliases":[],"aliases_ascii":[],"keywords":["space","sky","star"]},"star2":{"unicode":"1f31f","unicode_alt":"","code_decimal":"🌟","name":"glowing star","shortname":":star2:","category":"nature","emoji_order":"1728","aliases":[],"aliases_ascii":[],"keywords":["space","sky","star"]},"stars":{"unicode":"1f320","unicode_alt":"","code_decimal":"🌠","name":"shooting star","shortname":":stars:","category":"travel","emoji_order":"1729","aliases":[],"aliases_ascii":[],"keywords":["space"]},"cloud":{"unicode":"2601","unicode_alt":"2601-fe0f","code_decimal":"☁","name":"cloud","shortname":":cloud:","category":"nature","emoji_order":"1730","aliases":[],"aliases_ascii":[],"keywords":["weather","sky","cloud","cold","rain"]},"partly_sunny":{"unicode":"26c5","unicode_alt":"26c5-fe0f","code_decimal":"⛅","name":"sun behind cloud","shortname":":partly_sunny:","category":"nature","emoji_order":"1731","aliases":[],"aliases_ascii":[],"keywords":["weather","sky","cloud","sun"]},"thunder_cloud_rain":{"unicode":"26c8","unicode_alt":"26c8-fe0f","code_decimal":"⛈","name":"thunder cloud and rain","shortname":":thunder_cloud_rain:","category":"nature","emoji_order":"1732","aliases":[":thunder_cloud_and_rain:"],"aliases_ascii":[],"keywords":["weather","sky","cloud","cold","rain"]},"white_sun_small_cloud":{"unicode":"1f324","unicode_alt":"1f324-fe0f","code_decimal":"🌤","name":"white sun with small cloud","shortname":":white_sun_small_cloud:","category":"nature","emoji_order":"1733","aliases":[":white_sun_with_small_cloud:"],"aliases_ascii":[],"keywords":["weather","sky","cloud","sun"]},"white_sun_cloud":{"unicode":"1f325","unicode_alt":"1f325-fe0f","code_decimal":"🌥","name":"white sun behind cloud","shortname":":white_sun_cloud:","category":"nature","emoji_order":"1734","aliases":[":white_sun_behind_cloud:"],"aliases_ascii":[],"keywords":["weather","sky","cloud","cold","sun"]},"white_sun_rain_cloud":{"unicode":"1f326","unicode_alt":"1f326-fe0f","code_decimal":"🌦","name":"white sun behind cloud with rain","shortname":":white_sun_rain_cloud:","category":"nature","emoji_order":"1735","aliases":[":white_sun_behind_cloud_with_rain:"],"aliases_ascii":[],"keywords":["weather","sky","cloud","cold","rain","sun"]},"cloud_rain":{"unicode":"1f327","unicode_alt":"1f327-fe0f","code_decimal":"🌧","name":"cloud with rain","shortname":":cloud_rain:","category":"nature","emoji_order":"1736","aliases":[":cloud_with_rain:"],"aliases_ascii":[],"keywords":["weather","winter","sky","cloud","cold","rain"]},"cloud_snow":{"unicode":"1f328","unicode_alt":"1f328-fe0f","code_decimal":"🌨","name":"cloud with snow","shortname":":cloud_snow:","category":"nature","emoji_order":"1737","aliases":[":cloud_with_snow:"],"aliases_ascii":[],"keywords":["weather","winter","sky","cloud","cold","snow"]},"cloud_lightning":{"unicode":"1f329","unicode_alt":"1f329-fe0f","code_decimal":"🌩","name":"cloud with lightning","shortname":":cloud_lightning:","category":"nature","emoji_order":"1738","aliases":[":cloud_with_lightning:"],"aliases_ascii":[],"keywords":["weather","sky","cloud","cold","rain"]},"cloud_tornado":{"unicode":"1f32a","unicode_alt":"1f32a-fe0f","code_decimal":"🌪","name":"cloud with tornado","shortname":":cloud_tornado:","category":"nature","emoji_order":"1739","aliases":[":cloud_with_tornado:"],"aliases_ascii":[],"keywords":["weather","sky","cold"]},"fog":{"unicode":"1f32b","unicode_alt":"1f32b-fe0f","code_decimal":"🌫","name":"fog","shortname":":fog:","category":"nature","emoji_order":"1740","aliases":[],"aliases_ascii":[],"keywords":["weather","sky","cold"]},"wind_blowing_face":{"unicode":"1f32c","unicode_alt":"1f32c-fe0f","code_decimal":"🌬","name":"wind blowing face","shortname":":wind_blowing_face:","category":"nature","emoji_order":"1741","aliases":[],"aliases_ascii":[],"keywords":["weather","cold"]},"cyclone":{"unicode":"1f300","unicode_alt":"","code_decimal":"🌀","name":"cyclone","shortname":":cyclone:","category":"symbols","emoji_order":"1742","aliases":[],"aliases_ascii":[],"keywords":["symbol","drugs"]},"rainbow":{"unicode":"1f308","unicode_alt":"","code_decimal":"🌈","name":"rainbow","shortname":":rainbow:","category":"travel","emoji_order":"1743","aliases":[],"aliases_ascii":[],"keywords":["weather","gay","sky","rain"]},"closed_umbrella":{"unicode":"1f302","unicode_alt":"","code_decimal":"🌂","name":"closed umbrella","shortname":":closed_umbrella:","category":"people","emoji_order":"1744","aliases":[],"aliases_ascii":[],"keywords":["object","sky","rain","accessories"]},"umbrella2":{"unicode":"2602","unicode_alt":"2602-fe0f","code_decimal":"☂","name":"umbrella","shortname":":umbrella2:","category":"nature","emoji_order":"1745","aliases":[],"aliases_ascii":[],"keywords":["weather","object","sky","cold"]},"umbrella":{"unicode":"2614","unicode_alt":"2614-fe0f","code_decimal":"☔","name":"umbrella with rain drops","shortname":":umbrella:","category":"nature","emoji_order":"1746","aliases":[],"aliases_ascii":[],"keywords":["weather","sky","cold","rain"]},"beach_umbrella":{"unicode":"26f1","unicode_alt":"26f1-fe0f","code_decimal":"⛱","name":"umbrella on ground","shortname":":beach_umbrella:","category":"objects","emoji_order":"1747","aliases":[":umbrella_on_ground:"],"aliases_ascii":[],"keywords":["travel","vacation","tropical"]},"zap":{"unicode":"26a1","unicode_alt":"26a1-fe0f","code_decimal":"⚡","name":"high voltage sign","shortname":":zap:","category":"nature","emoji_order":"1748","aliases":[],"aliases_ascii":[],"keywords":["weather","sky","diarrhea"]},"snowflake":{"unicode":"2744","unicode_alt":"2744-fe0f","code_decimal":"❄","name":"snowflake","shortname":":snowflake:","category":"nature","emoji_order":"1749","aliases":[],"aliases_ascii":[],"keywords":["weather","winter","sky","holidays","cold","snow"]},"snowman2":{"unicode":"2603","unicode_alt":"2603-fe0f","code_decimal":"☃","name":"snowman","shortname":":snowman2:","category":"nature","emoji_order":"1750","aliases":[],"aliases_ascii":[],"keywords":["weather","winter","holidays","christmas","cold","snow"]},"snowman":{"unicode":"26c4","unicode_alt":"26c4-fe0f","code_decimal":"⛄","name":"snowman without snow","shortname":":snowman:","category":"nature","emoji_order":"1751","aliases":[],"aliases_ascii":[],"keywords":["weather","winter","holidays","cold","snow"]},"comet":{"unicode":"2604","unicode_alt":"2604-fe0f","code_decimal":"☄","name":"comet","shortname":":comet:","category":"nature","emoji_order":"1752","aliases":[],"aliases_ascii":[],"keywords":["space","sky"]},"fire":{"unicode":"1f525","unicode_alt":"","code_decimal":"🔥","name":"fire","shortname":":fire:","category":"nature","emoji_order":"1753","aliases":[":flame:"],"aliases_ascii":[],"keywords":["wth","hot"]},"droplet":{"unicode":"1f4a7","unicode_alt":"","code_decimal":"💧","name":"droplet","shortname":":droplet:","category":"nature","emoji_order":"1754","aliases":[],"aliases_ascii":[],"keywords":["weather","sky","rain"]},"ocean":{"unicode":"1f30a","unicode_alt":"","code_decimal":"🌊","name":"water wave","shortname":":ocean:","category":"nature","emoji_order":"1755","aliases":[],"aliases_ascii":[],"keywords":["weather","boat","tropical","swim"]},"jack_o_lantern":{"unicode":"1f383","unicode_alt":"","code_decimal":"🎃","name":"jack-o-lantern","shortname":":jack_o_lantern:","category":"nature","emoji_order":"1756","aliases":[],"aliases_ascii":[],"keywords":["holidays","halloween"]},"christmas_tree":{"unicode":"1f384","unicode_alt":"","code_decimal":"🎄","name":"christmas tree","shortname":":christmas_tree:","category":"nature","emoji_order":"1757","aliases":[],"aliases_ascii":[],"keywords":["plant","holidays","christmas","trees"]},"fireworks":{"unicode":"1f386","unicode_alt":"","code_decimal":"🎆","name":"fireworks","shortname":":fireworks:","category":"travel","emoji_order":"1758","aliases":[],"aliases_ascii":[],"keywords":["parties"]},"sparkler":{"unicode":"1f387","unicode_alt":"","code_decimal":"🎇","name":"firework sparkler","shortname":":sparkler:","category":"travel","emoji_order":"1759","aliases":[],"aliases_ascii":[],"keywords":["parties"]},"sparkles":{"unicode":"2728","unicode_alt":"","code_decimal":"✨","name":"sparkles","shortname":":sparkles:","category":"nature","emoji_order":"1760","aliases":[],"aliases_ascii":[],"keywords":["star","girls night"]},"balloon":{"unicode":"1f388","unicode_alt":"","code_decimal":"🎈","name":"balloon","shortname":":balloon:","category":"objects","emoji_order":"1761","aliases":[],"aliases_ascii":[],"keywords":["object","birthday","good","parties"]},"tada":{"unicode":"1f389","unicode_alt":"","code_decimal":"🎉","name":"party popper","shortname":":tada:","category":"objects","emoji_order":"1762","aliases":[],"aliases_ascii":[],"keywords":["object","birthday","holidays","cheers","good","girls night","boys night","parties"]},"confetti_ball":{"unicode":"1f38a","unicode_alt":"","code_decimal":"🎊","name":"confetti ball","shortname":":confetti_ball:","category":"objects","emoji_order":"1763","aliases":[],"aliases_ascii":[],"keywords":["object","birthday","holidays","cheers","girls night","boys night","parties"]},"tanabata_tree":{"unicode":"1f38b","unicode_alt":"","code_decimal":"🎋","name":"tanabata tree","shortname":":tanabata_tree:","category":"nature","emoji_order":"1764","aliases":[],"aliases_ascii":[],"keywords":["nature","plant","trees"]},"bamboo":{"unicode":"1f38d","unicode_alt":"","code_decimal":"🎍","name":"pine decoration","shortname":":bamboo:","category":"nature","emoji_order":"1765","aliases":[],"aliases_ascii":[],"keywords":["nature","plant"]},"dolls":{"unicode":"1f38e","unicode_alt":"","code_decimal":"🎎","name":"japanese dolls","shortname":":dolls:","category":"objects","emoji_order":"1766","aliases":[],"aliases_ascii":[],"keywords":["people","japan"]},"flags":{"unicode":"1f38f","unicode_alt":"","code_decimal":"🎏","name":"carp streamer","shortname":":flags:","category":"objects","emoji_order":"1767","aliases":[],"aliases_ascii":[],"keywords":["object","japan"]},"wind_chime":{"unicode":"1f390","unicode_alt":"","code_decimal":"🎐","name":"wind chime","shortname":":wind_chime:","category":"objects","emoji_order":"1768","aliases":[],"aliases_ascii":[],"keywords":["object","japan"]},"rice_scene":{"unicode":"1f391","unicode_alt":"","code_decimal":"🎑","name":"moon viewing ceremony","shortname":":rice_scene:","category":"travel","emoji_order":"1769","aliases":[],"aliases_ascii":[],"keywords":["places","space","sky","travel"]},"ribbon":{"unicode":"1f380","unicode_alt":"","code_decimal":"🎀","name":"ribbon","shortname":":ribbon:","category":"objects","emoji_order":"1770","aliases":[],"aliases_ascii":[],"keywords":["object","gift","birthday"]},"gift":{"unicode":"1f381","unicode_alt":"","code_decimal":"🎁","name":"wrapped present","shortname":":gift:","category":"objects","emoji_order":"1771","aliases":[],"aliases_ascii":[],"keywords":["object","gift","birthday","holidays","christmas","parties"]},"reminder_ribbon":{"unicode":"1f397","unicode_alt":"1f397-fe0f","code_decimal":"🎗","name":"reminder ribbon","shortname":":reminder_ribbon:","category":"activity","emoji_order":"1772","aliases":[],"aliases_ascii":[],"keywords":["award"]},"tickets":{"unicode":"1f39f","unicode_alt":"1f39f-fe0f","code_decimal":"🎟","name":"admission tickets","shortname":":tickets:","category":"activity","emoji_order":"1773","aliases":[":admission_tickets:"],"aliases_ascii":[],"keywords":["theatre","movie","parties"]},"ticket":{"unicode":"1f3ab","unicode_alt":"","code_decimal":"🎫","name":"ticket","shortname":":ticket:","category":"activity","emoji_order":"1774","aliases":[],"aliases_ascii":[],"keywords":["theatre","movie","parties"]},"military_medal":{"unicode":"1f396","unicode_alt":"1f396-fe0f","code_decimal":"🎖","name":"military medal","shortname":":military_medal:","category":"activity","emoji_order":"1775","aliases":[],"aliases_ascii":[],"keywords":["object","award","win"]},"trophy":{"unicode":"1f3c6","unicode_alt":"","code_decimal":"🏆","name":"trophy","shortname":":trophy:","category":"activity","emoji_order":"1776","aliases":[],"aliases_ascii":[],"keywords":["object","game","award","win","perfect","parties"]},"medal":{"unicode":"1f3c5","unicode_alt":"","code_decimal":"🏅","name":"sports medal","shortname":":medal:","category":"activity","emoji_order":"1777","aliases":[":sports_medal:"],"aliases_ascii":[],"keywords":["object","award","sport","win","perfect"]},"first_place":{"unicode":"1f947","unicode_alt":"","code_decimal":"🥇","name":"first place medal","shortname":":first_place:","category":"activity","emoji_order":"1778","aliases":[":first_place_medal:"],"aliases_ascii":[],"keywords":[]},"second_place":{"unicode":"1f948","unicode_alt":"","code_decimal":"🥈","name":"second place medal","shortname":":second_place:","category":"activity","emoji_order":"1779","aliases":[":second_place_medal:"],"aliases_ascii":[],"keywords":[]},"third_place":{"unicode":"1f949","unicode_alt":"","code_decimal":"🥉","name":"third place medal","shortname":":third_place:","category":"activity","emoji_order":"1780","aliases":[":third_place_medal:"],"aliases_ascii":[],"keywords":[]},"soccer":{"unicode":"26bd","unicode_alt":"26bd-fe0f","code_decimal":"⚽","name":"soccer ball","shortname":":soccer:","category":"activity","emoji_order":"1781","aliases":[],"aliases_ascii":[],"keywords":["game","ball","sport","soccer","football"]},"baseball":{"unicode":"26be","unicode_alt":"26be-fe0f","code_decimal":"⚾","name":"baseball","shortname":":baseball:","category":"activity","emoji_order":"1782","aliases":[],"aliases_ascii":[],"keywords":["game","ball","sport","baseball"]},"basketball":{"unicode":"1f3c0","unicode_alt":"","code_decimal":"🏀","name":"basketball and hoop","shortname":":basketball:","category":"activity","emoji_order":"1783","aliases":[],"aliases_ascii":[],"keywords":["game","ball","sport","basketball"]},"volleyball":{"unicode":"1f3d0","unicode_alt":"","code_decimal":"🏐","name":"volleyball","shortname":":volleyball:","category":"activity","emoji_order":"1784","aliases":[],"aliases_ascii":[],"keywords":["game","ball","sport","volleyball"]},"football":{"unicode":"1f3c8","unicode_alt":"","code_decimal":"🏈","name":"american football","shortname":":football:","category":"activity","emoji_order":"1785","aliases":[],"aliases_ascii":[],"keywords":["america","game","ball","sport","football"]},"rugby_football":{"unicode":"1f3c9","unicode_alt":"","code_decimal":"🏉","name":"rugby football","shortname":":rugby_football:","category":"activity","emoji_order":"1786","aliases":[],"aliases_ascii":[],"keywords":["game","sport","football"]},"tennis":{"unicode":"1f3be","unicode_alt":"","code_decimal":"🎾","name":"tennis racquet and ball","shortname":":tennis:","category":"activity","emoji_order":"1787","aliases":[],"aliases_ascii":[],"keywords":["game","ball","sport","tennis"]},"8ball":{"unicode":"1f3b1","unicode_alt":"","code_decimal":"🎱","name":"billiards","shortname":":8ball:","category":"activity","emoji_order":"1788","aliases":[],"aliases_ascii":[],"keywords":["game","ball","sport","billiards","luck","boys night"]},"bowling":{"unicode":"1f3b3","unicode_alt":"","code_decimal":"🎳","name":"bowling","shortname":":bowling:","category":"activity","emoji_order":"1789","aliases":[],"aliases_ascii":[],"keywords":["game","ball","sport","boys night"]},"cricket":{"unicode":"1f3cf","unicode_alt":"","code_decimal":"🏏","name":"cricket bat and ball","shortname":":cricket:","category":"activity","emoji_order":"1790","aliases":[":cricket_bat_ball:"],"aliases_ascii":[],"keywords":["ball","sport","cricket"]},"field_hockey":{"unicode":"1f3d1","unicode_alt":"","code_decimal":"🏑","name":"field hockey stick and ball","shortname":":field_hockey:","category":"activity","emoji_order":"1791","aliases":[],"aliases_ascii":[],"keywords":["ball","sport","hockey"]},"hockey":{"unicode":"1f3d2","unicode_alt":"","code_decimal":"🏒","name":"ice hockey stick and puck","shortname":":hockey:","category":"activity","emoji_order":"1792","aliases":[],"aliases_ascii":[],"keywords":["game","sport","hockey"]},"ping_pong":{"unicode":"1f3d3","unicode_alt":"","code_decimal":"🏓","name":"table tennis paddle and ball","shortname":":ping_pong:","category":"activity","emoji_order":"1793","aliases":[":table_tennis:"],"aliases_ascii":[],"keywords":["game","ball","sport","ping pong"]},"badminton":{"unicode":"1f3f8","unicode_alt":"","code_decimal":"🏸","name":"badminton racquet","shortname":":badminton:","category":"activity","emoji_order":"1794","aliases":[],"aliases_ascii":[],"keywords":["game","sport","badminton"]},"boxing_glove":{"unicode":"1f94a","unicode_alt":"","code_decimal":"🥊","name":"boxing glove","shortname":":boxing_glove:","category":"activity","emoji_order":"1795","aliases":[":boxing_gloves:"],"aliases_ascii":[],"keywords":[]},"martial_arts_uniform":{"unicode":"1f94b","unicode_alt":"","code_decimal":"🥋","name":"martial arts uniform","shortname":":martial_arts_uniform:","category":"activity","emoji_order":"1796","aliases":[":karate_uniform:"],"aliases_ascii":[],"keywords":[]},"goal":{"unicode":"1f945","unicode_alt":"","code_decimal":"🥅","name":"goal net","shortname":":goal:","category":"activity","emoji_order":"1797","aliases":[":goal_net:"],"aliases_ascii":[],"keywords":[]},"dart":{"unicode":"1f3af","unicode_alt":"","code_decimal":"🎯","name":"direct hit","shortname":":dart:","category":"activity","emoji_order":"1798","aliases":[],"aliases_ascii":[],"keywords":["game","sport","boys night"]},"golf":{"unicode":"26f3","unicode_alt":"26f3-fe0f","code_decimal":"⛳","name":"flag in hole","shortname":":golf:","category":"activity","emoji_order":"1799","aliases":[],"aliases_ascii":[],"keywords":["game","ball","vacation","sport","golf"]},"ice_skate":{"unicode":"26f8","unicode_alt":"26f8-fe0f","code_decimal":"⛸","name":"ice skate","shortname":":ice_skate:","category":"activity","emoji_order":"1800","aliases":[],"aliases_ascii":[],"keywords":["cold","sport","ice skating"]},"fishing_pole_and_fish":{"unicode":"1f3a3","unicode_alt":"","code_decimal":"🎣","name":"fishing pole and fish","shortname":":fishing_pole_and_fish:","category":"activity","emoji_order":"1801","aliases":[],"aliases_ascii":[],"keywords":["vacation","sport","fishing"]},"running_shirt_with_sash":{"unicode":"1f3bd","unicode_alt":"","code_decimal":"🎽","name":"running shirt with sash","shortname":":running_shirt_with_sash:","category":"activity","emoji_order":"1802","aliases":[],"aliases_ascii":[],"keywords":["award"]},"ski":{"unicode":"1f3bf","unicode_alt":"","code_decimal":"🎿","name":"ski and ski boot","shortname":":ski:","category":"activity","emoji_order":"1803","aliases":[],"aliases_ascii":[],"keywords":["cold","sport","skiing"]},"video_game":{"unicode":"1f3ae","unicode_alt":"","code_decimal":"🎮","name":"video game","shortname":":video_game:","category":"activity","emoji_order":"1804","aliases":[],"aliases_ascii":[],"keywords":["electronics","game","boys night"]},"joystick":{"unicode":"1f579","unicode_alt":"1f579-fe0f","code_decimal":"🕹","name":"joystick","shortname":":joystick:","category":"objects","emoji_order":"1805","aliases":[],"aliases_ascii":[],"keywords":["electronics","game","boys night"]},"game_die":{"unicode":"1f3b2","unicode_alt":"","code_decimal":"🎲","name":"game die","shortname":":game_die:","category":"activity","emoji_order":"1806","aliases":[],"aliases_ascii":[],"keywords":["object","game","boys night"]},"spades":{"unicode":"2660","unicode_alt":"2660-fe0f","code_decimal":"♠","name":"black spade suit","shortname":":spades:","category":"symbols","emoji_order":"1807","aliases":[],"aliases_ascii":[],"keywords":["symbol","game"]},"hearts":{"unicode":"2665","unicode_alt":"2665-fe0f","code_decimal":"♥","name":"black heart suit","shortname":":hearts:","category":"symbols","emoji_order":"1808","aliases":[],"aliases_ascii":[],"keywords":["love","symbol","game"]},"diamonds":{"unicode":"2666","unicode_alt":"2666-fe0f","code_decimal":"♦","name":"black diamond suit","shortname":":diamonds:","category":"symbols","emoji_order":"1809","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","game"]},"clubs":{"unicode":"2663","unicode_alt":"2663-fe0f","code_decimal":"♣","name":"black club suit","shortname":":clubs:","category":"symbols","emoji_order":"1810","aliases":[],"aliases_ascii":[],"keywords":["symbol","game"]},"black_joker":{"unicode":"1f0cf","unicode_alt":"","code_decimal":"🃏","name":"playing card black joker","shortname":":black_joker:","category":"symbols","emoji_order":"1811","aliases":[],"aliases_ascii":[],"keywords":["object","symbol","game"]},"mahjong":{"unicode":"1f004","unicode_alt":"1f004-fe0f","code_decimal":"🀄","name":"mahjong tile red dragon","shortname":":mahjong:","category":"symbols","emoji_order":"1812","aliases":[],"aliases_ascii":[],"keywords":["object","symbol","game"]},"flower_playing_cards":{"unicode":"1f3b4","unicode_alt":"","code_decimal":"🎴","name":"flower playing cards","shortname":":flower_playing_cards:","category":"symbols","emoji_order":"1813","aliases":[],"aliases_ascii":[],"keywords":["object","symbol"]},"mute":{"unicode":"1f507","unicode_alt":"","code_decimal":"🔇","name":"speaker with cancellation stroke","shortname":":mute:","category":"symbols","emoji_order":"1814","aliases":[],"aliases_ascii":[],"keywords":["alarm","symbol"]},"speaker":{"unicode":"1f508","unicode_alt":"","code_decimal":"🔈","name":"speaker","shortname":":speaker:","category":"symbols","emoji_order":"1815","aliases":[],"aliases_ascii":[],"keywords":["alarm","symbol"]},"sound":{"unicode":"1f509","unicode_alt":"","code_decimal":"🔉","name":"speaker with one sound wave","shortname":":sound:","category":"symbols","emoji_order":"1816","aliases":[],"aliases_ascii":[],"keywords":["alarm","symbol"]},"loud_sound":{"unicode":"1f50a","unicode_alt":"","code_decimal":"🔊","name":"speaker with three sound waves","shortname":":loud_sound:","category":"symbols","emoji_order":"1817","aliases":[],"aliases_ascii":[],"keywords":["alarm","symbol"]},"loudspeaker":{"unicode":"1f4e2","unicode_alt":"","code_decimal":"📢","name":"public address loudspeaker","shortname":":loudspeaker:","category":"symbols","emoji_order":"1818","aliases":[],"aliases_ascii":[],"keywords":["object","alarm","symbol"]},"mega":{"unicode":"1f4e3","unicode_alt":"","code_decimal":"📣","name":"cheering megaphone","shortname":":mega:","category":"symbols","emoji_order":"1819","aliases":[],"aliases_ascii":[],"keywords":["object","sport"]},"postal_horn":{"unicode":"1f4ef","unicode_alt":"","code_decimal":"📯","name":"postal horn","shortname":":postal_horn:","category":"objects","emoji_order":"1820","aliases":[],"aliases_ascii":[],"keywords":["object"]},"bell":{"unicode":"1f514","unicode_alt":"","code_decimal":"🔔","name":"bell","shortname":":bell:","category":"symbols","emoji_order":"1821","aliases":[],"aliases_ascii":[],"keywords":["object","alarm","symbol"]},"no_bell":{"unicode":"1f515","unicode_alt":"","code_decimal":"🔕","name":"bell with cancellation stroke","shortname":":no_bell:","category":"symbols","emoji_order":"1822","aliases":[],"aliases_ascii":[],"keywords":["alarm","symbol"]},"musical_score":{"unicode":"1f3bc","unicode_alt":"","code_decimal":"🎼","name":"musical score","shortname":":musical_score:","category":"activity","emoji_order":"1823","aliases":[],"aliases_ascii":[],"keywords":["instruments"]},"musical_note":{"unicode":"1f3b5","unicode_alt":"","code_decimal":"🎵","name":"musical note","shortname":":musical_note:","category":"symbols","emoji_order":"1824","aliases":[],"aliases_ascii":[],"keywords":["instruments","symbol"]},"notes":{"unicode":"1f3b6","unicode_alt":"","code_decimal":"🎶","name":"multiple musical notes","shortname":":notes:","category":"symbols","emoji_order":"1825","aliases":[],"aliases_ascii":[],"keywords":["instruments","symbol"]},"microphone2":{"unicode":"1f399","unicode_alt":"1f399-fe0f","code_decimal":"🎙","name":"studio microphone","shortname":":microphone2:","category":"objects","emoji_order":"1826","aliases":[":studio_microphone:"],"aliases_ascii":[],"keywords":["electronics","object"]},"level_slider":{"unicode":"1f39a","unicode_alt":"1f39a-fe0f","code_decimal":"🎚","name":"level slider","shortname":":level_slider:","category":"objects","emoji_order":"1827","aliases":[],"aliases_ascii":[],"keywords":[]},"control_knobs":{"unicode":"1f39b","unicode_alt":"1f39b-fe0f","code_decimal":"🎛","name":"control knobs","shortname":":control_knobs:","category":"objects","emoji_order":"1828","aliases":[],"aliases_ascii":[],"keywords":["time"]},"microphone":{"unicode":"1f3a4","unicode_alt":"","code_decimal":"🎤","name":"microphone","shortname":":microphone:","category":"activity","emoji_order":"1829","aliases":[],"aliases_ascii":[],"keywords":["instruments"]},"headphones":{"unicode":"1f3a7","unicode_alt":"","code_decimal":"🎧","name":"headphone","shortname":":headphones:","category":"activity","emoji_order":"1830","aliases":[],"aliases_ascii":[],"keywords":["instruments"]},"radio":{"unicode":"1f4fb","unicode_alt":"","code_decimal":"📻","name":"radio","shortname":":radio:","category":"objects","emoji_order":"1831","aliases":[],"aliases_ascii":[],"keywords":["electronics"]},"saxophone":{"unicode":"1f3b7","unicode_alt":"","code_decimal":"🎷","name":"saxophone","shortname":":saxophone:","category":"activity","emoji_order":"1832","aliases":[],"aliases_ascii":[],"keywords":["instruments"]},"guitar":{"unicode":"1f3b8","unicode_alt":"","code_decimal":"🎸","name":"guitar","shortname":":guitar:","category":"activity","emoji_order":"1833","aliases":[],"aliases_ascii":[],"keywords":["instruments"]},"musical_keyboard":{"unicode":"1f3b9","unicode_alt":"","code_decimal":"🎹","name":"musical keyboard","shortname":":musical_keyboard:","category":"activity","emoji_order":"1834","aliases":[],"aliases_ascii":[],"keywords":["instruments"]},"trumpet":{"unicode":"1f3ba","unicode_alt":"","code_decimal":"🎺","name":"trumpet","shortname":":trumpet:","category":"activity","emoji_order":"1835","aliases":[],"aliases_ascii":[],"keywords":["instruments"]},"violin":{"unicode":"1f3bb","unicode_alt":"","code_decimal":"🎻","name":"violin","shortname":":violin:","category":"activity","emoji_order":"1836","aliases":[],"aliases_ascii":[],"keywords":["instruments","sarcastic"]},"drum":{"unicode":"1f941","unicode_alt":"","code_decimal":"🥁","name":"drum with drumsticks","shortname":":drum:","category":"activity","emoji_order":"1837","aliases":[":drum_with_drumsticks:"],"aliases_ascii":[],"keywords":[]},"iphone":{"unicode":"1f4f1","unicode_alt":"","code_decimal":"📱","name":"mobile phone","shortname":":iphone:","category":"objects","emoji_order":"1838","aliases":[],"aliases_ascii":[],"keywords":["electronics","phone","selfie"]},"calling":{"unicode":"1f4f2","unicode_alt":"","code_decimal":"📲","name":"mobile phone with rightwards arrow at left","shortname":":calling:","category":"objects","emoji_order":"1839","aliases":[],"aliases_ascii":[],"keywords":["electronics","phone","selfie"]},"telephone":{"unicode":"260e","unicode_alt":"260e-fe0f","code_decimal":"☎","name":"black telephone","shortname":":telephone:","category":"objects","emoji_order":"1840","aliases":[],"aliases_ascii":[],"keywords":["electronics","phone"]},"telephone_receiver":{"unicode":"1f4de","unicode_alt":"","code_decimal":"📞","name":"telephone receiver","shortname":":telephone_receiver:","category":"objects","emoji_order":"1841","aliases":[],"aliases_ascii":[],"keywords":["electronics","phone"]},"pager":{"unicode":"1f4df","unicode_alt":"","code_decimal":"📟","name":"pager","shortname":":pager:","category":"objects","emoji_order":"1842","aliases":[],"aliases_ascii":[],"keywords":["electronics","work"]},"fax":{"unicode":"1f4e0","unicode_alt":"","code_decimal":"📠","name":"fax machine","shortname":":fax:","category":"objects","emoji_order":"1843","aliases":[],"aliases_ascii":[],"keywords":["electronics","work","office"]},"battery":{"unicode":"1f50b","unicode_alt":"","code_decimal":"🔋","name":"battery","shortname":":battery:","category":"objects","emoji_order":"1844","aliases":[],"aliases_ascii":[],"keywords":["object"]},"electric_plug":{"unicode":"1f50c","unicode_alt":"","code_decimal":"🔌","name":"electric plug","shortname":":electric_plug:","category":"objects","emoji_order":"1845","aliases":[],"aliases_ascii":[],"keywords":["electronics"]},"computer":{"unicode":"1f4bb","unicode_alt":"","code_decimal":"💻","name":"personal computer","shortname":":computer:","category":"objects","emoji_order":"1846","aliases":[],"aliases_ascii":[],"keywords":["electronics","work","office"]},"desktop":{"unicode":"1f5a5","unicode_alt":"1f5a5-fe0f","code_decimal":"🖥","name":"desktop computer","shortname":":desktop:","category":"objects","emoji_order":"1847","aliases":[":desktop_computer:"],"aliases_ascii":[],"keywords":["electronics","work"]},"printer":{"unicode":"1f5a8","unicode_alt":"1f5a8-fe0f","code_decimal":"🖨","name":"printer","shortname":":printer:","category":"objects","emoji_order":"1848","aliases":[],"aliases_ascii":[],"keywords":["electronics","work","office"]},"keyboard":{"unicode":"2328","unicode_alt":"2328-fe0f","code_decimal":"⌨","name":"keyboard","shortname":":keyboard:","category":"objects","emoji_order":"1849","aliases":[],"aliases_ascii":[],"keywords":["electronics","work","office"]},"mouse_three_button":{"unicode":"1f5b1","unicode_alt":"1f5b1-fe0f","code_decimal":"🖱","name":"three button mouse","shortname":":mouse_three_button:","category":"objects","emoji_order":"1850","aliases":[":three_button_mouse:"],"aliases_ascii":[],"keywords":["electronics","work","game","office"]},"trackball":{"unicode":"1f5b2","unicode_alt":"1f5b2-fe0f","code_decimal":"🖲","name":"trackball","shortname":":trackball:","category":"objects","emoji_order":"1851","aliases":[],"aliases_ascii":[],"keywords":["electronics","work","game","office"]},"minidisc":{"unicode":"1f4bd","unicode_alt":"","code_decimal":"💽","name":"minidisc","shortname":":minidisc:","category":"objects","emoji_order":"1852","aliases":[],"aliases_ascii":[],"keywords":["electronics"]},"floppy_disk":{"unicode":"1f4be","unicode_alt":"","code_decimal":"💾","name":"floppy disk","shortname":":floppy_disk:","category":"objects","emoji_order":"1853","aliases":[],"aliases_ascii":[],"keywords":["electronics","office"]},"cd":{"unicode":"1f4bf","unicode_alt":"","code_decimal":"💿","name":"optical disc","shortname":":cd:","category":"objects","emoji_order":"1854","aliases":[],"aliases_ascii":[],"keywords":["electronics"]},"dvd":{"unicode":"1f4c0","unicode_alt":"","code_decimal":"📀","name":"dvd","shortname":":dvd:","category":"objects","emoji_order":"1855","aliases":[],"aliases_ascii":[],"keywords":["electronics"]},"movie_camera":{"unicode":"1f3a5","unicode_alt":"","code_decimal":"🎥","name":"movie camera","shortname":":movie_camera:","category":"objects","emoji_order":"1856","aliases":[],"aliases_ascii":[],"keywords":["object","camera","movie"]},"film_frames":{"unicode":"1f39e","unicode_alt":"1f39e-fe0f","code_decimal":"🎞","name":"film frames","shortname":":film_frames:","category":"objects","emoji_order":"1857","aliases":[],"aliases_ascii":[],"keywords":["object","camera","movie"]},"projector":{"unicode":"1f4fd","unicode_alt":"1f4fd-fe0f","code_decimal":"📽","name":"film projector","shortname":":projector:","category":"objects","emoji_order":"1858","aliases":[":film_projector:"],"aliases_ascii":[],"keywords":["object","camera","movie"]},"clapper":{"unicode":"1f3ac","unicode_alt":"","code_decimal":"🎬","name":"clapper board","shortname":":clapper:","category":"activity","emoji_order":"1859","aliases":[],"aliases_ascii":[],"keywords":["movie"]},"tv":{"unicode":"1f4fa","unicode_alt":"","code_decimal":"📺","name":"television","shortname":":tv:","category":"objects","emoji_order":"1860","aliases":[],"aliases_ascii":[],"keywords":["electronics"]},"camera":{"unicode":"1f4f7","unicode_alt":"","code_decimal":"📷","name":"camera","shortname":":camera:","category":"objects","emoji_order":"1861","aliases":[],"aliases_ascii":[],"keywords":["electronics","camera","selfie"]},"camera_with_flash":{"unicode":"1f4f8","unicode_alt":"","code_decimal":"📸","name":"camera with flash","shortname":":camera_with_flash:","category":"objects","emoji_order":"1862","aliases":[],"aliases_ascii":[],"keywords":["electronics","camera"]},"video_camera":{"unicode":"1f4f9","unicode_alt":"","code_decimal":"📹","name":"video camera","shortname":":video_camera:","category":"objects","emoji_order":"1863","aliases":[],"aliases_ascii":[],"keywords":["electronics","camera","movie"]},"vhs":{"unicode":"1f4fc","unicode_alt":"","code_decimal":"📼","name":"videocassette","shortname":":vhs:","category":"objects","emoji_order":"1864","aliases":[],"aliases_ascii":[],"keywords":["electronics"]},"mag":{"unicode":"1f50d","unicode_alt":"","code_decimal":"🔍","name":"left-pointing magnifying glass","shortname":":mag:","category":"objects","emoji_order":"1865","aliases":[],"aliases_ascii":[],"keywords":["object"]},"mag_right":{"unicode":"1f50e","unicode_alt":"","code_decimal":"🔎","name":"right-pointing magnifying glass","shortname":":mag_right:","category":"objects","emoji_order":"1866","aliases":[],"aliases_ascii":[],"keywords":["object"]},"microscope":{"unicode":"1f52c","unicode_alt":"","code_decimal":"🔬","name":"microscope","shortname":":microscope:","category":"objects","emoji_order":"1867","aliases":[],"aliases_ascii":[],"keywords":["object","science"]},"telescope":{"unicode":"1f52d","unicode_alt":"","code_decimal":"🔭","name":"telescope","shortname":":telescope:","category":"objects","emoji_order":"1868","aliases":[],"aliases_ascii":[],"keywords":["object","space","science"]},"satellite":{"unicode":"1f4e1","unicode_alt":"","code_decimal":"📡","name":"satellite antenna","shortname":":satellite:","category":"objects","emoji_order":"1869","aliases":[],"aliases_ascii":[],"keywords":["object"]},"candle":{"unicode":"1f56f","unicode_alt":"1f56f-fe0f","code_decimal":"🕯","name":"candle","shortname":":candle:","category":"objects","emoji_order":"1870","aliases":[],"aliases_ascii":[],"keywords":["object"]},"bulb":{"unicode":"1f4a1","unicode_alt":"","code_decimal":"💡","name":"electric light bulb","shortname":":bulb:","category":"objects","emoji_order":"1871","aliases":[],"aliases_ascii":[],"keywords":["object","science"]},"flashlight":{"unicode":"1f526","unicode_alt":"","code_decimal":"🔦","name":"electric torch","shortname":":flashlight:","category":"objects","emoji_order":"1872","aliases":[],"aliases_ascii":[],"keywords":["electronics","object"]},"izakaya_lantern":{"unicode":"1f3ee","unicode_alt":"","code_decimal":"🏮","name":"izakaya lantern","shortname":":izakaya_lantern:","category":"objects","emoji_order":"1873","aliases":[],"aliases_ascii":[],"keywords":["object","japan"]},"notebook_with_decorative_cover":{"unicode":"1f4d4","unicode_alt":"","code_decimal":"📔","name":"notebook with decorative cover","shortname":":notebook_with_decorative_cover:","category":"objects","emoji_order":"1874","aliases":[],"aliases_ascii":[],"keywords":["object","office","write"]},"closed_book":{"unicode":"1f4d5","unicode_alt":"","code_decimal":"📕","name":"closed book","shortname":":closed_book:","category":"objects","emoji_order":"1875","aliases":[],"aliases_ascii":[],"keywords":["object","office","write","book"]},"book":{"unicode":"1f4d6","unicode_alt":"","code_decimal":"📖","name":"open book","shortname":":book:","category":"objects","emoji_order":"1876","aliases":[],"aliases_ascii":[],"keywords":["object","office","write","book"]},"green_book":{"unicode":"1f4d7","unicode_alt":"","code_decimal":"📗","name":"green book","shortname":":green_book:","category":"objects","emoji_order":"1877","aliases":[],"aliases_ascii":[],"keywords":["object","office","book"]},"blue_book":{"unicode":"1f4d8","unicode_alt":"","code_decimal":"📘","name":"blue book","shortname":":blue_book:","category":"objects","emoji_order":"1878","aliases":[],"aliases_ascii":[],"keywords":["object","office","write","book"]},"orange_book":{"unicode":"1f4d9","unicode_alt":"","code_decimal":"📙","name":"orange book","shortname":":orange_book:","category":"objects","emoji_order":"1879","aliases":[],"aliases_ascii":[],"keywords":["object","office","write","book"]},"books":{"unicode":"1f4da","unicode_alt":"","code_decimal":"📚","name":"books","shortname":":books:","category":"objects","emoji_order":"1880","aliases":[],"aliases_ascii":[],"keywords":["object","office","write","book"]},"notebook":{"unicode":"1f4d3","unicode_alt":"","code_decimal":"📓","name":"notebook","shortname":":notebook:","category":"objects","emoji_order":"1881","aliases":[],"aliases_ascii":[],"keywords":["object","office","write"]},"ledger":{"unicode":"1f4d2","unicode_alt":"","code_decimal":"📒","name":"ledger","shortname":":ledger:","category":"objects","emoji_order":"1882","aliases":[],"aliases_ascii":[],"keywords":["object","office","write"]},"page_with_curl":{"unicode":"1f4c3","unicode_alt":"","code_decimal":"📃","name":"page with curl","shortname":":page_with_curl:","category":"objects","emoji_order":"1883","aliases":[],"aliases_ascii":[],"keywords":["office","write"]},"scroll":{"unicode":"1f4dc","unicode_alt":"","code_decimal":"📜","name":"scroll","shortname":":scroll:","category":"objects","emoji_order":"1884","aliases":[],"aliases_ascii":[],"keywords":["object","office"]},"page_facing_up":{"unicode":"1f4c4","unicode_alt":"","code_decimal":"📄","name":"page facing up","shortname":":page_facing_up:","category":"objects","emoji_order":"1885","aliases":[],"aliases_ascii":[],"keywords":["work","office","write"]},"newspaper":{"unicode":"1f4f0","unicode_alt":"","code_decimal":"📰","name":"newspaper","shortname":":newspaper:","category":"objects","emoji_order":"1886","aliases":[],"aliases_ascii":[],"keywords":["office","write"]},"newspaper2":{"unicode":"1f5de","unicode_alt":"1f5de-fe0f","code_decimal":"🗞","name":"rolled-up newspaper","shortname":":newspaper2:","category":"objects","emoji_order":"1887","aliases":[":rolled_up_newspaper:"],"aliases_ascii":[],"keywords":["office","write"]},"bookmark_tabs":{"unicode":"1f4d1","unicode_alt":"","code_decimal":"📑","name":"bookmark tabs","shortname":":bookmark_tabs:","category":"objects","emoji_order":"1888","aliases":[],"aliases_ascii":[],"keywords":["office","write"]},"bookmark":{"unicode":"1f516","unicode_alt":"","code_decimal":"🔖","name":"bookmark","shortname":":bookmark:","category":"objects","emoji_order":"1889","aliases":[],"aliases_ascii":[],"keywords":["object","book"]},"label":{"unicode":"1f3f7","unicode_alt":"1f3f7-fe0f","code_decimal":"🏷","name":"label","shortname":":label:","category":"objects","emoji_order":"1890","aliases":[],"aliases_ascii":[],"keywords":["object"]},"moneybag":{"unicode":"1f4b0","unicode_alt":"","code_decimal":"💰","name":"money bag","shortname":":moneybag:","category":"objects","emoji_order":"1891","aliases":[],"aliases_ascii":[],"keywords":["bag","award","money"]},"yen":{"unicode":"1f4b4","unicode_alt":"","code_decimal":"💴","name":"banknote with yen sign","shortname":":yen:","category":"objects","emoji_order":"1892","aliases":[],"aliases_ascii":[],"keywords":["money"]},"dollar":{"unicode":"1f4b5","unicode_alt":"","code_decimal":"💵","name":"banknote with dollar sign","shortname":":dollar:","category":"objects","emoji_order":"1893","aliases":[],"aliases_ascii":[],"keywords":["money"]},"euro":{"unicode":"1f4b6","unicode_alt":"","code_decimal":"💶","name":"banknote with euro sign","shortname":":euro:","category":"objects","emoji_order":"1894","aliases":[],"aliases_ascii":[],"keywords":["money"]},"pound":{"unicode":"1f4b7","unicode_alt":"","code_decimal":"💷","name":"banknote with pound sign","shortname":":pound:","category":"objects","emoji_order":"1895","aliases":[],"aliases_ascii":[],"keywords":["money"]},"money_with_wings":{"unicode":"1f4b8","unicode_alt":"","code_decimal":"💸","name":"money with wings","shortname":":money_with_wings:","category":"objects","emoji_order":"1896","aliases":[],"aliases_ascii":[],"keywords":["money","boys night"]},"credit_card":{"unicode":"1f4b3","unicode_alt":"","code_decimal":"💳","name":"credit card","shortname":":credit_card:","category":"objects","emoji_order":"1897","aliases":[],"aliases_ascii":[],"keywords":["object","money","boys night"]},"chart":{"unicode":"1f4b9","unicode_alt":"","code_decimal":"💹","name":"chart with upwards trend and yen sign","shortname":":chart:","category":"symbols","emoji_order":"1898","aliases":[],"aliases_ascii":[],"keywords":["symbol","money"]},"currency_exchange":{"unicode":"1f4b1","unicode_alt":"","code_decimal":"💱","name":"currency exchange","shortname":":currency_exchange:","category":"symbols","emoji_order":"1899","aliases":[],"aliases_ascii":[],"keywords":["symbol","money"]},"heavy_dollar_sign":{"unicode":"1f4b2","unicode_alt":"","code_decimal":"💲","name":"heavy dollar sign","shortname":":heavy_dollar_sign:","category":"symbols","emoji_order":"1900","aliases":[],"aliases_ascii":[],"keywords":["math","symbol","money"]},"envelope":{"unicode":"2709","unicode_alt":"2709-fe0f","code_decimal":"✉","name":"envelope","shortname":":envelope:","category":"objects","emoji_order":"1901","aliases":[],"aliases_ascii":[],"keywords":["object","office","write"]},"e-mail":{"unicode":"1f4e7","unicode_alt":"","code_decimal":"📧","name":"e-mail symbol","shortname":":e-mail:","category":"objects","emoji_order":"1902","aliases":[":email:"],"aliases_ascii":[],"keywords":["office"]},"incoming_envelope":{"unicode":"1f4e8","unicode_alt":"","code_decimal":"📨","name":"incoming envelope","shortname":":incoming_envelope:","category":"objects","emoji_order":"1903","aliases":[],"aliases_ascii":[],"keywords":["object"]},"envelope_with_arrow":{"unicode":"1f4e9","unicode_alt":"","code_decimal":"📩","name":"envelope with downwards arrow above","shortname":":envelope_with_arrow:","category":"objects","emoji_order":"1904","aliases":[],"aliases_ascii":[],"keywords":["object","office"]},"outbox_tray":{"unicode":"1f4e4","unicode_alt":"","code_decimal":"📤","name":"outbox tray","shortname":":outbox_tray:","category":"objects","emoji_order":"1905","aliases":[],"aliases_ascii":[],"keywords":["work","office"]},"inbox_tray":{"unicode":"1f4e5","unicode_alt":"","code_decimal":"📥","name":"inbox tray","shortname":":inbox_tray:","category":"objects","emoji_order":"1906","aliases":[],"aliases_ascii":[],"keywords":["work","office"]},"package":{"unicode":"1f4e6","unicode_alt":"","code_decimal":"📦","name":"package","shortname":":package:","category":"objects","emoji_order":"1907","aliases":[],"aliases_ascii":[],"keywords":["object","gift","office"]},"mailbox":{"unicode":"1f4eb","unicode_alt":"","code_decimal":"📫","name":"closed mailbox with raised flag","shortname":":mailbox:","category":"objects","emoji_order":"1908","aliases":[],"aliases_ascii":[],"keywords":["object"]},"mailbox_closed":{"unicode":"1f4ea","unicode_alt":"","code_decimal":"📪","name":"closed mailbox with lowered flag","shortname":":mailbox_closed:","category":"objects","emoji_order":"1909","aliases":[],"aliases_ascii":[],"keywords":["object","office"]},"mailbox_with_mail":{"unicode":"1f4ec","unicode_alt":"","code_decimal":"📬","name":"open mailbox with raised flag","shortname":":mailbox_with_mail:","category":"objects","emoji_order":"1910","aliases":[],"aliases_ascii":[],"keywords":["object"]},"mailbox_with_no_mail":{"unicode":"1f4ed","unicode_alt":"","code_decimal":"📭","name":"open mailbox with lowered flag","shortname":":mailbox_with_no_mail:","category":"objects","emoji_order":"1911","aliases":[],"aliases_ascii":[],"keywords":["object"]},"postbox":{"unicode":"1f4ee","unicode_alt":"","code_decimal":"📮","name":"postbox","shortname":":postbox:","category":"objects","emoji_order":"1912","aliases":[],"aliases_ascii":[],"keywords":["object"]},"ballot_box":{"unicode":"1f5f3","unicode_alt":"1f5f3-fe0f","code_decimal":"🗳","name":"ballot box with ballot","shortname":":ballot_box:","category":"objects","emoji_order":"1913","aliases":[":ballot_box_with_ballot:"],"aliases_ascii":[],"keywords":["object","office"]},"pencil2":{"unicode":"270f","unicode_alt":"270f-fe0f","code_decimal":"✏","name":"pencil","shortname":":pencil2:","category":"objects","emoji_order":"1914","aliases":[],"aliases_ascii":[],"keywords":["object","office","write"]},"black_nib":{"unicode":"2712","unicode_alt":"2712-fe0f","code_decimal":"✒","name":"black nib","shortname":":black_nib:","category":"objects","emoji_order":"1915","aliases":[],"aliases_ascii":[],"keywords":["object","office","write"]},"pen_fountain":{"unicode":"1f58b","unicode_alt":"1f58b-fe0f","code_decimal":"🖋","name":"lower left fountain pen","shortname":":pen_fountain:","category":"objects","emoji_order":"1916","aliases":[":lower_left_fountain_pen:"],"aliases_ascii":[],"keywords":["object","office","write"]},"pen_ballpoint":{"unicode":"1f58a","unicode_alt":"1f58a-fe0f","code_decimal":"🖊","name":"lower left ballpoint pen","shortname":":pen_ballpoint:","category":"objects","emoji_order":"1917","aliases":[":lower_left_ballpoint_pen:"],"aliases_ascii":[],"keywords":["object","office","write"]},"paintbrush":{"unicode":"1f58c","unicode_alt":"1f58c-fe0f","code_decimal":"🖌","name":"lower left paintbrush","shortname":":paintbrush:","category":"objects","emoji_order":"1918","aliases":[":lower_left_paintbrush:"],"aliases_ascii":[],"keywords":["object","office","write"]},"crayon":{"unicode":"1f58d","unicode_alt":"1f58d-fe0f","code_decimal":"🖍","name":"lower left crayon","shortname":":crayon:","category":"objects","emoji_order":"1919","aliases":[":lower_left_crayon:"],"aliases_ascii":[],"keywords":["object","office","write"]},"pencil":{"unicode":"1f4dd","unicode_alt":"","code_decimal":"📝","name":"memo","shortname":":pencil:","category":"objects","emoji_order":"1920","aliases":[],"aliases_ascii":[],"keywords":["work","office","write"]},"briefcase":{"unicode":"1f4bc","unicode_alt":"","code_decimal":"💼","name":"briefcase","shortname":":briefcase:","category":"people","emoji_order":"1921","aliases":[],"aliases_ascii":[],"keywords":["bag","work","accessories","nutcase","job"]},"file_folder":{"unicode":"1f4c1","unicode_alt":"","code_decimal":"📁","name":"file folder","shortname":":file_folder:","category":"objects","emoji_order":"1922","aliases":[],"aliases_ascii":[],"keywords":["work","office"]},"open_file_folder":{"unicode":"1f4c2","unicode_alt":"","code_decimal":"📂","name":"open file folder","shortname":":open_file_folder:","category":"objects","emoji_order":"1923","aliases":[],"aliases_ascii":[],"keywords":["work","office"]},"dividers":{"unicode":"1f5c2","unicode_alt":"1f5c2-fe0f","code_decimal":"🗂","name":"card index dividers","shortname":":dividers:","category":"objects","emoji_order":"1924","aliases":[":card_index_dividers:"],"aliases_ascii":[],"keywords":["work","office"]},"date":{"unicode":"1f4c5","unicode_alt":"","code_decimal":"📅","name":"calendar","shortname":":date:","category":"objects","emoji_order":"1925","aliases":[],"aliases_ascii":[],"keywords":["object","office"]},"calendar":{"unicode":"1f4c6","unicode_alt":"","code_decimal":"📆","name":"tear-off calendar","shortname":":calendar:","category":"objects","emoji_order":"1926","aliases":[],"aliases_ascii":[],"keywords":["object","office"]},"notepad_spiral":{"unicode":"1f5d2","unicode_alt":"1f5d2-fe0f","code_decimal":"🗒","name":"spiral note pad","shortname":":notepad_spiral:","category":"objects","emoji_order":"1927","aliases":[":spiral_note_pad:"],"aliases_ascii":[],"keywords":["work","office","write"]},"calendar_spiral":{"unicode":"1f5d3","unicode_alt":"1f5d3-fe0f","code_decimal":"🗓","name":"spiral calendar pad","shortname":":calendar_spiral:","category":"objects","emoji_order":"1928","aliases":[":spiral_calendar_pad:"],"aliases_ascii":[],"keywords":["object","office"]},"card_index":{"unicode":"1f4c7","unicode_alt":"","code_decimal":"📇","name":"card index","shortname":":card_index:","category":"objects","emoji_order":"1929","aliases":[],"aliases_ascii":[],"keywords":["object","work","office"]},"chart_with_upwards_trend":{"unicode":"1f4c8","unicode_alt":"","code_decimal":"📈","name":"chart with upwards trend","shortname":":chart_with_upwards_trend:","category":"objects","emoji_order":"1930","aliases":[],"aliases_ascii":[],"keywords":["work","office"]},"chart_with_downwards_trend":{"unicode":"1f4c9","unicode_alt":"","code_decimal":"📉","name":"chart with downwards trend","shortname":":chart_with_downwards_trend:","category":"objects","emoji_order":"1931","aliases":[],"aliases_ascii":[],"keywords":["work","office"]},"bar_chart":{"unicode":"1f4ca","unicode_alt":"","code_decimal":"📊","name":"bar chart","shortname":":bar_chart:","category":"objects","emoji_order":"1932","aliases":[],"aliases_ascii":[],"keywords":["work","office"]},"clipboard":{"unicode":"1f4cb","unicode_alt":"","code_decimal":"📋","name":"clipboard","shortname":":clipboard:","category":"objects","emoji_order":"1933","aliases":[],"aliases_ascii":[],"keywords":["object","work","office","write"]},"pushpin":{"unicode":"1f4cc","unicode_alt":"","code_decimal":"📌","name":"pushpin","shortname":":pushpin:","category":"objects","emoji_order":"1934","aliases":[],"aliases_ascii":[],"keywords":["object","office"]},"round_pushpin":{"unicode":"1f4cd","unicode_alt":"","code_decimal":"📍","name":"round pushpin","shortname":":round_pushpin:","category":"objects","emoji_order":"1935","aliases":[],"aliases_ascii":[],"keywords":["object","office"]},"paperclip":{"unicode":"1f4ce","unicode_alt":"","code_decimal":"📎","name":"paperclip","shortname":":paperclip:","category":"objects","emoji_order":"1936","aliases":[],"aliases_ascii":[],"keywords":["object","work","office"]},"paperclips":{"unicode":"1f587","unicode_alt":"1f587-fe0f","code_decimal":"🖇","name":"linked paperclips","shortname":":paperclips:","category":"objects","emoji_order":"1937","aliases":[":linked_paperclips:"],"aliases_ascii":[],"keywords":["object","work","office"]},"straight_ruler":{"unicode":"1f4cf","unicode_alt":"","code_decimal":"📏","name":"straight ruler","shortname":":straight_ruler:","category":"objects","emoji_order":"1938","aliases":[],"aliases_ascii":[],"keywords":["object","tool","office"]},"triangular_ruler":{"unicode":"1f4d0","unicode_alt":"","code_decimal":"📐","name":"triangular ruler","shortname":":triangular_ruler:","category":"objects","emoji_order":"1939","aliases":[],"aliases_ascii":[],"keywords":["object","tool","office"]},"scissors":{"unicode":"2702","unicode_alt":"2702-fe0f","code_decimal":"✂","name":"black scissors","shortname":":scissors:","category":"objects","emoji_order":"1940","aliases":[],"aliases_ascii":[],"keywords":["object","tool","weapon","office"]},"card_box":{"unicode":"1f5c3","unicode_alt":"1f5c3-fe0f","code_decimal":"🗃","name":"card file box","shortname":":card_box:","category":"objects","emoji_order":"1941","aliases":[":card_file_box:"],"aliases_ascii":[],"keywords":["object","work","office"]},"file_cabinet":{"unicode":"1f5c4","unicode_alt":"1f5c4-fe0f","code_decimal":"🗄","name":"file cabinet","shortname":":file_cabinet:","category":"objects","emoji_order":"1942","aliases":[],"aliases_ascii":[],"keywords":["object","work","office"]},"wastebasket":{"unicode":"1f5d1","unicode_alt":"1f5d1-fe0f","code_decimal":"🗑","name":"wastebasket","shortname":":wastebasket:","category":"objects","emoji_order":"1943","aliases":[],"aliases_ascii":[],"keywords":["object","work"]},"lock":{"unicode":"1f512","unicode_alt":"","code_decimal":"🔒","name":"lock","shortname":":lock:","category":"objects","emoji_order":"1944","aliases":[],"aliases_ascii":[],"keywords":["object","lock"]},"unlock":{"unicode":"1f513","unicode_alt":"","code_decimal":"🔓","name":"open lock","shortname":":unlock:","category":"objects","emoji_order":"1945","aliases":[],"aliases_ascii":[],"keywords":["object","lock"]},"lock_with_ink_pen":{"unicode":"1f50f","unicode_alt":"","code_decimal":"🔏","name":"lock with ink pen","shortname":":lock_with_ink_pen:","category":"objects","emoji_order":"1946","aliases":[],"aliases_ascii":[],"keywords":["object","lock"]},"closed_lock_with_key":{"unicode":"1f510","unicode_alt":"","code_decimal":"🔐","name":"closed lock with key","shortname":":closed_lock_with_key:","category":"objects","emoji_order":"1947","aliases":[],"aliases_ascii":[],"keywords":["object","lock"]},"key":{"unicode":"1f511","unicode_alt":"","code_decimal":"🔑","name":"key","shortname":":key:","category":"objects","emoji_order":"1948","aliases":[],"aliases_ascii":[],"keywords":["object","lock"]},"key2":{"unicode":"1f5dd","unicode_alt":"1f5dd-fe0f","code_decimal":"🗝","name":"old key","shortname":":key2:","category":"objects","emoji_order":"1949","aliases":[":old_key:"],"aliases_ascii":[],"keywords":["object","lock"]},"hammer":{"unicode":"1f528","unicode_alt":"","code_decimal":"🔨","name":"hammer","shortname":":hammer:","category":"objects","emoji_order":"1950","aliases":[],"aliases_ascii":[],"keywords":["object","tool","weapon"]},"pick":{"unicode":"26cf","unicode_alt":"26cf-fe0f","code_decimal":"⛏","name":"pick","shortname":":pick:","category":"objects","emoji_order":"1951","aliases":[],"aliases_ascii":[],"keywords":["object","tool","weapon"]},"hammer_pick":{"unicode":"2692","unicode_alt":"2692-fe0f","code_decimal":"⚒","name":"hammer and pick","shortname":":hammer_pick:","category":"objects","emoji_order":"1952","aliases":[":hammer_and_pick:"],"aliases_ascii":[],"keywords":["object","tool","weapon"]},"tools":{"unicode":"1f6e0","unicode_alt":"1f6e0-fe0f","code_decimal":"🛠","name":"hammer and wrench","shortname":":tools:","category":"objects","emoji_order":"1953","aliases":[":hammer_and_wrench:"],"aliases_ascii":[],"keywords":["object","tool"]},"dagger":{"unicode":"1f5e1","unicode_alt":"1f5e1-fe0f","code_decimal":"🗡","name":"dagger knife","shortname":":dagger:","category":"objects","emoji_order":"1954","aliases":[":dagger_knife:"],"aliases_ascii":[],"keywords":["object","weapon"]},"crossed_swords":{"unicode":"2694","unicode_alt":"2694-fe0f","code_decimal":"⚔","name":"crossed swords","shortname":":crossed_swords:","category":"objects","emoji_order":"1955","aliases":[],"aliases_ascii":[],"keywords":["object","weapon"]},"gun":{"unicode":"1f52b","unicode_alt":"","code_decimal":"🔫","name":"pistol","shortname":":gun:","category":"objects","emoji_order":"1956","aliases":[],"aliases_ascii":[],"keywords":["object","weapon","dead","gun","sarcastic"]},"bow_and_arrow":{"unicode":"1f3f9","unicode_alt":"","code_decimal":"🏹","name":"bow and arrow","shortname":":bow_and_arrow:","category":"activity","emoji_order":"1957","aliases":[":archery:"],"aliases_ascii":[],"keywords":["weapon","sport"]},"shield":{"unicode":"1f6e1","unicode_alt":"1f6e1-fe0f","code_decimal":"🛡","name":"shield","shortname":":shield:","category":"objects","emoji_order":"1958","aliases":[],"aliases_ascii":[],"keywords":["object"]},"wrench":{"unicode":"1f527","unicode_alt":"","code_decimal":"🔧","name":"wrench","shortname":":wrench:","category":"objects","emoji_order":"1959","aliases":[],"aliases_ascii":[],"keywords":["object","tool"]},"nut_and_bolt":{"unicode":"1f529","unicode_alt":"","code_decimal":"🔩","name":"nut and bolt","shortname":":nut_and_bolt:","category":"objects","emoji_order":"1960","aliases":[],"aliases_ascii":[],"keywords":["object","tool","nutcase"]},"gear":{"unicode":"2699","unicode_alt":"2699-fe0f","code_decimal":"⚙","name":"gear","shortname":":gear:","category":"objects","emoji_order":"1961","aliases":[],"aliases_ascii":[],"keywords":["object","tool"]},"compression":{"unicode":"1f5dc","unicode_alt":"1f5dc-fe0f","code_decimal":"🗜","name":"compression","shortname":":compression:","category":"objects","emoji_order":"1962","aliases":[],"aliases_ascii":[],"keywords":[]},"alembic":{"unicode":"2697","unicode_alt":"2697-fe0f","code_decimal":"⚗","name":"alembic","shortname":":alembic:","category":"objects","emoji_order":"1963","aliases":[],"aliases_ascii":[],"keywords":["object","science"]},"scales":{"unicode":"2696","unicode_alt":"2696-fe0f","code_decimal":"⚖","name":"scales","shortname":":scales:","category":"objects","emoji_order":"1964","aliases":[],"aliases_ascii":[],"keywords":["object"]},"link":{"unicode":"1f517","unicode_alt":"","code_decimal":"🔗","name":"link symbol","shortname":":link:","category":"objects","emoji_order":"1965","aliases":[],"aliases_ascii":[],"keywords":["symbol","office"]},"chains":{"unicode":"26d3","unicode_alt":"26d3-fe0f","code_decimal":"⛓","name":"chains","shortname":":chains:","category":"objects","emoji_order":"1966","aliases":[],"aliases_ascii":[],"keywords":["object","tool"]},"syringe":{"unicode":"1f489","unicode_alt":"","code_decimal":"💉","name":"syringe","shortname":":syringe:","category":"objects","emoji_order":"1967","aliases":[],"aliases_ascii":[],"keywords":["object","weapon","health","drugs"]},"pill":{"unicode":"1f48a","unicode_alt":"","code_decimal":"💊","name":"pill","shortname":":pill:","category":"objects","emoji_order":"1968","aliases":[],"aliases_ascii":[],"keywords":["object","health","drugs"]},"smoking":{"unicode":"1f6ac","unicode_alt":"","code_decimal":"🚬","name":"smoking symbol","shortname":":smoking:","category":"objects","emoji_order":"1969","aliases":[],"aliases_ascii":[],"keywords":["symbol","drugs","smoking"]},"coffin":{"unicode":"26b0","unicode_alt":"26b0-fe0f","code_decimal":"⚰","name":"coffin","shortname":":coffin:","category":"objects","emoji_order":"1970","aliases":[],"aliases_ascii":[],"keywords":["object","dead","rip"]},"urn":{"unicode":"26b1","unicode_alt":"26b1-fe0f","code_decimal":"⚱","name":"funeral urn","shortname":":urn:","category":"objects","emoji_order":"1971","aliases":[":funeral_urn:"],"aliases_ascii":[],"keywords":["object","dead","rip"]},"moyai":{"unicode":"1f5ff","unicode_alt":"","code_decimal":"🗿","name":"moyai","shortname":":moyai:","category":"objects","emoji_order":"1972","aliases":[],"aliases_ascii":[],"keywords":["travel","vacation"]},"oil":{"unicode":"1f6e2","unicode_alt":"1f6e2-fe0f","code_decimal":"🛢","name":"oil drum","shortname":":oil:","category":"objects","emoji_order":"1973","aliases":[":oil_drum:"],"aliases_ascii":[],"keywords":["object"]},"crystal_ball":{"unicode":"1f52e","unicode_alt":"","code_decimal":"🔮","name":"crystal ball","shortname":":crystal_ball:","category":"objects","emoji_order":"1974","aliases":[],"aliases_ascii":[],"keywords":["object","ball"]},"shopping_cart":{"unicode":"1f6d2","unicode_alt":"","code_decimal":"🛒","name":"shopping trolley","shortname":":shopping_cart:","category":"objects","emoji_order":"1975","aliases":[":shopping_trolley:"],"aliases_ascii":[],"keywords":[]},"atm":{"unicode":"1f3e7","unicode_alt":"","code_decimal":"🏧","name":"automated teller machine","shortname":":atm:","category":"symbols","emoji_order":"1976","aliases":[],"aliases_ascii":[],"keywords":["electronics","symbol","money"]},"put_litter_in_its_place":{"unicode":"1f6ae","unicode_alt":"","code_decimal":"🚮","name":"put litter in its place symbol","shortname":":put_litter_in_its_place:","category":"symbols","emoji_order":"1977","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"potable_water":{"unicode":"1f6b0","unicode_alt":"","code_decimal":"🚰","name":"potable water symbol","shortname":":potable_water:","category":"symbols","emoji_order":"1978","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"wheelchair":{"unicode":"267f","unicode_alt":"267f-fe0f","code_decimal":"♿","name":"wheelchair symbol","shortname":":wheelchair:","category":"symbols","emoji_order":"1979","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"mens":{"unicode":"1f6b9","unicode_alt":"","code_decimal":"🚹","name":"mens symbol","shortname":":mens:","category":"symbols","emoji_order":"1980","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"womens":{"unicode":"1f6ba","unicode_alt":"","code_decimal":"🚺","name":"womens symbol","shortname":":womens:","category":"symbols","emoji_order":"1981","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"restroom":{"unicode":"1f6bb","unicode_alt":"","code_decimal":"🚻","name":"restroom","shortname":":restroom:","category":"symbols","emoji_order":"1982","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"baby_symbol":{"unicode":"1f6bc","unicode_alt":"","code_decimal":"🚼","name":"baby symbol","shortname":":baby_symbol:","category":"symbols","emoji_order":"1983","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"wc":{"unicode":"1f6be","unicode_alt":"","code_decimal":"🚾","name":"water closet","shortname":":wc:","category":"symbols","emoji_order":"1984","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"passport_control":{"unicode":"1f6c2","unicode_alt":"","code_decimal":"🛂","name":"passport control","shortname":":passport_control:","category":"symbols","emoji_order":"1985","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"customs":{"unicode":"1f6c3","unicode_alt":"","code_decimal":"🛃","name":"customs","shortname":":customs:","category":"symbols","emoji_order":"1986","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"baggage_claim":{"unicode":"1f6c4","unicode_alt":"","code_decimal":"🛄","name":"baggage claim","shortname":":baggage_claim:","category":"symbols","emoji_order":"1987","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"left_luggage":{"unicode":"1f6c5","unicode_alt":"","code_decimal":"🛅","name":"left luggage","shortname":":left_luggage:","category":"symbols","emoji_order":"1988","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"warning":{"unicode":"26a0","unicode_alt":"26a0-fe0f","code_decimal":"⚠","name":"warning sign","shortname":":warning:","category":"symbols","emoji_order":"1989","aliases":[],"aliases_ascii":[],"keywords":["symbol","punctuation"]},"children_crossing":{"unicode":"1f6b8","unicode_alt":"","code_decimal":"🚸","name":"children crossing","shortname":":children_crossing:","category":"symbols","emoji_order":"1990","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"no_entry":{"unicode":"26d4","unicode_alt":"26d4-fe0f","code_decimal":"⛔","name":"no entry","shortname":":no_entry:","category":"symbols","emoji_order":"1991","aliases":[],"aliases_ascii":[],"keywords":["symbol","circle"]},"no_entry_sign":{"unicode":"1f6ab","unicode_alt":"","code_decimal":"🚫","name":"no entry sign","shortname":":no_entry_sign:","category":"symbols","emoji_order":"1992","aliases":[],"aliases_ascii":[],"keywords":["symbol","circle"]},"no_bicycles":{"unicode":"1f6b3","unicode_alt":"","code_decimal":"🚳","name":"no bicycles","shortname":":no_bicycles:","category":"symbols","emoji_order":"1993","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"no_smoking":{"unicode":"1f6ad","unicode_alt":"","code_decimal":"🚭","name":"no smoking symbol","shortname":":no_smoking:","category":"symbols","emoji_order":"1994","aliases":[],"aliases_ascii":[],"keywords":["symbol","smoking"]},"do_not_litter":{"unicode":"1f6af","unicode_alt":"","code_decimal":"🚯","name":"do not litter symbol","shortname":":do_not_litter:","category":"symbols","emoji_order":"1995","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"non-potable_water":{"unicode":"1f6b1","unicode_alt":"","code_decimal":"🚱","name":"non-potable water symbol","shortname":":non-potable_water:","category":"symbols","emoji_order":"1996","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"no_pedestrians":{"unicode":"1f6b7","unicode_alt":"","code_decimal":"🚷","name":"no pedestrians","shortname":":no_pedestrians:","category":"symbols","emoji_order":"1997","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"no_mobile_phones":{"unicode":"1f4f5","unicode_alt":"","code_decimal":"📵","name":"no mobile phones","shortname":":no_mobile_phones:","category":"symbols","emoji_order":"1998","aliases":[],"aliases_ascii":[],"keywords":["symbol","phone"]},"underage":{"unicode":"1f51e","unicode_alt":"","code_decimal":"🔞","name":"no one under eighteen symbol","shortname":":underage:","category":"symbols","emoji_order":"1999","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"radioactive":{"unicode":"2622","unicode_alt":"2622-fe0f","code_decimal":"☢","name":"radioactive sign","shortname":":radioactive:","category":"symbols","emoji_order":"2000","aliases":[":radioactive_sign:"],"aliases_ascii":[],"keywords":["symbol","science"]},"biohazard":{"unicode":"2623","unicode_alt":"2623-fe0f","code_decimal":"☣","name":"biohazard sign","shortname":":biohazard:","category":"symbols","emoji_order":"2001","aliases":[":biohazard_sign:"],"aliases_ascii":[],"keywords":["symbol","science"]},"arrow_up":{"unicode":"2b06","unicode_alt":"2b06-fe0f","code_decimal":"⬆","name":"upwards black arrow","shortname":":arrow_up:","category":"symbols","emoji_order":"2002","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_upper_right":{"unicode":"2197","unicode_alt":"2197-fe0f","code_decimal":"↗","name":"north east arrow","shortname":":arrow_upper_right:","category":"symbols","emoji_order":"2003","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_right":{"unicode":"27a1","unicode_alt":"27a1-fe0f","code_decimal":"➡","name":"black rightwards arrow","shortname":":arrow_right:","category":"symbols","emoji_order":"2004","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_lower_right":{"unicode":"2198","unicode_alt":"2198-fe0f","code_decimal":"↘","name":"south east arrow","shortname":":arrow_lower_right:","category":"symbols","emoji_order":"2005","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_down":{"unicode":"2b07","unicode_alt":"2b07-fe0f","code_decimal":"⬇","name":"downwards black arrow","shortname":":arrow_down:","category":"symbols","emoji_order":"2006","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_lower_left":{"unicode":"2199","unicode_alt":"2199-fe0f","code_decimal":"↙","name":"south west arrow","shortname":":arrow_lower_left:","category":"symbols","emoji_order":"2007","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_left":{"unicode":"2b05","unicode_alt":"2b05-fe0f","code_decimal":"⬅","name":"leftwards black arrow","shortname":":arrow_left:","category":"symbols","emoji_order":"2008","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_upper_left":{"unicode":"2196","unicode_alt":"2196-fe0f","code_decimal":"↖","name":"north west arrow","shortname":":arrow_upper_left:","category":"symbols","emoji_order":"2009","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_up_down":{"unicode":"2195","unicode_alt":"2195-fe0f","code_decimal":"↕","name":"up down arrow","shortname":":arrow_up_down:","category":"symbols","emoji_order":"2010","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"left_right_arrow":{"unicode":"2194","unicode_alt":"2194-fe0f","code_decimal":"↔","name":"left right arrow","shortname":":left_right_arrow:","category":"symbols","emoji_order":"2011","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"leftwards_arrow_with_hook":{"unicode":"21a9","unicode_alt":"21a9-fe0f","code_decimal":"↩","name":"leftwards arrow with hook","shortname":":leftwards_arrow_with_hook:","category":"symbols","emoji_order":"2012","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_right_hook":{"unicode":"21aa","unicode_alt":"21aa-fe0f","code_decimal":"↪","name":"rightwards arrow with hook","shortname":":arrow_right_hook:","category":"symbols","emoji_order":"2013","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_heading_up":{"unicode":"2934","unicode_alt":"2934-fe0f","code_decimal":"⤴","name":"arrow pointing rightwards then curving upwards","shortname":":arrow_heading_up:","category":"symbols","emoji_order":"2014","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_heading_down":{"unicode":"2935","unicode_alt":"2935-fe0f","code_decimal":"⤵","name":"arrow pointing rightwards then curving downwards","shortname":":arrow_heading_down:","category":"symbols","emoji_order":"2015","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrows_clockwise":{"unicode":"1f503","unicode_alt":"","code_decimal":"🔃","name":"clockwise downwards and upwards open circle arrows","shortname":":arrows_clockwise:","category":"symbols","emoji_order":"2016","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrows_counterclockwise":{"unicode":"1f504","unicode_alt":"","code_decimal":"🔄","name":"anticlockwise downwards and upwards open circle arrows","shortname":":arrows_counterclockwise:","category":"symbols","emoji_order":"2017","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"back":{"unicode":"1f519","unicode_alt":"","code_decimal":"🔙","name":"back with leftwards arrow above","shortname":":back:","category":"symbols","emoji_order":"2018","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"end":{"unicode":"1f51a","unicode_alt":"","code_decimal":"🔚","name":"end with leftwards arrow above","shortname":":end:","category":"symbols","emoji_order":"2019","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"on":{"unicode":"1f51b","unicode_alt":"","code_decimal":"🔛","name":"on with exclamation mark with left right arrow abo","shortname":":on:","category":"symbols","emoji_order":"2020","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"soon":{"unicode":"1f51c","unicode_alt":"","code_decimal":"🔜","name":"soon with rightwards arrow above","shortname":":soon:","category":"symbols","emoji_order":"2021","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"top":{"unicode":"1f51d","unicode_alt":"","code_decimal":"🔝","name":"top with upwards arrow above","shortname":":top:","category":"symbols","emoji_order":"2022","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"place_of_worship":{"unicode":"1f6d0","unicode_alt":"","code_decimal":"🛐","name":"place of worship","shortname":":place_of_worship:","category":"symbols","emoji_order":"2023","aliases":[":worship_symbol:"],"aliases_ascii":[],"keywords":["religion","symbol","pray"]},"atom":{"unicode":"269b","unicode_alt":"269b-fe0f","code_decimal":"⚛","name":"atom symbol","shortname":":atom:","category":"symbols","emoji_order":"2024","aliases":[":atom_symbol:"],"aliases_ascii":[],"keywords":["symbol","science"]},"om_symbol":{"unicode":"1f549","unicode_alt":"1f549-fe0f","code_decimal":"🕉","name":"om symbol","shortname":":om_symbol:","category":"symbols","emoji_order":"2025","aliases":[],"aliases_ascii":[],"keywords":["religion","symbol"]},"star_of_david":{"unicode":"2721","unicode_alt":"2721-fe0f","code_decimal":"✡","name":"star of david","shortname":":star_of_david:","category":"symbols","emoji_order":"2026","aliases":[],"aliases_ascii":[],"keywords":["religion","jew","star","symbol"]},"wheel_of_dharma":{"unicode":"2638","unicode_alt":"2638-fe0f","code_decimal":"☸","name":"wheel of dharma","shortname":":wheel_of_dharma:","category":"symbols","emoji_order":"2027","aliases":[],"aliases_ascii":[],"keywords":["religion","symbol"]},"yin_yang":{"unicode":"262f","unicode_alt":"262f-fe0f","code_decimal":"☯","name":"yin yang","shortname":":yin_yang:","category":"symbols","emoji_order":"2028","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"cross":{"unicode":"271d","unicode_alt":"271d-fe0f","code_decimal":"✝","name":"latin cross","shortname":":cross:","category":"symbols","emoji_order":"2029","aliases":[":latin_cross:"],"aliases_ascii":[],"keywords":["religion","symbol"]},"orthodox_cross":{"unicode":"2626","unicode_alt":"2626-fe0f","code_decimal":"☦","name":"orthodox cross","shortname":":orthodox_cross:","category":"symbols","emoji_order":"2030","aliases":[],"aliases_ascii":[],"keywords":["religion","symbol"]},"star_and_crescent":{"unicode":"262a","unicode_alt":"262a-fe0f","code_decimal":"☪","name":"star and crescent","shortname":":star_and_crescent:","category":"symbols","emoji_order":"2031","aliases":[],"aliases_ascii":[],"keywords":["religion","symbol"]},"peace":{"unicode":"262e","unicode_alt":"262e-fe0f","code_decimal":"☮","name":"peace symbol","shortname":":peace:","category":"symbols","emoji_order":"2032","aliases":[":peace_symbol:"],"aliases_ascii":[],"keywords":["symbol","peace","drugs"]},"menorah":{"unicode":"1f54e","unicode_alt":"","code_decimal":"🕎","name":"menorah with nine branches","shortname":":menorah:","category":"symbols","emoji_order":"2033","aliases":[],"aliases_ascii":[],"keywords":["religion","object","jew","symbol","holidays"]},"six_pointed_star":{"unicode":"1f52f","unicode_alt":"","code_decimal":"🔯","name":"six pointed star with middle dot","shortname":":six_pointed_star:","category":"symbols","emoji_order":"2034","aliases":[],"aliases_ascii":[],"keywords":["religion","jew","star","symbol"]},"aries":{"unicode":"2648","unicode_alt":"2648-fe0f","code_decimal":"♈","name":"aries","shortname":":aries:","category":"symbols","emoji_order":"2035","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"taurus":{"unicode":"2649","unicode_alt":"2649-fe0f","code_decimal":"♉","name":"taurus","shortname":":taurus:","category":"symbols","emoji_order":"2036","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"gemini":{"unicode":"264a","unicode_alt":"264a-fe0f","code_decimal":"♊","name":"gemini","shortname":":gemini:","category":"symbols","emoji_order":"2037","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"cancer":{"unicode":"264b","unicode_alt":"264b-fe0f","code_decimal":"♋","name":"cancer","shortname":":cancer:","category":"symbols","emoji_order":"2038","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"leo":{"unicode":"264c","unicode_alt":"264c-fe0f","code_decimal":"♌","name":"leo","shortname":":leo:","category":"symbols","emoji_order":"2039","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"virgo":{"unicode":"264d","unicode_alt":"264d-fe0f","code_decimal":"♍","name":"virgo","shortname":":virgo:","category":"symbols","emoji_order":"2040","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"libra":{"unicode":"264e","unicode_alt":"264e-fe0f","code_decimal":"♎","name":"libra","shortname":":libra:","category":"symbols","emoji_order":"2041","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"scorpius":{"unicode":"264f","unicode_alt":"264f-fe0f","code_decimal":"♏","name":"scorpius","shortname":":scorpius:","category":"symbols","emoji_order":"2042","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"sagittarius":{"unicode":"2650","unicode_alt":"2650-fe0f","code_decimal":"♐","name":"sagittarius","shortname":":sagittarius:","category":"symbols","emoji_order":"2043","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"capricorn":{"unicode":"2651","unicode_alt":"2651-fe0f","code_decimal":"♑","name":"capricorn","shortname":":capricorn:","category":"symbols","emoji_order":"2044","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"aquarius":{"unicode":"2652","unicode_alt":"2652-fe0f","code_decimal":"♒","name":"aquarius","shortname":":aquarius:","category":"symbols","emoji_order":"2045","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"pisces":{"unicode":"2653","unicode_alt":"2653-fe0f","code_decimal":"♓","name":"pisces","shortname":":pisces:","category":"symbols","emoji_order":"2046","aliases":[],"aliases_ascii":[],"keywords":["zodiac","symbol"]},"ophiuchus":{"unicode":"26ce","unicode_alt":"","code_decimal":"⛎","name":"ophiuchus","shortname":":ophiuchus:","category":"symbols","emoji_order":"2047","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"twisted_rightwards_arrows":{"unicode":"1f500","unicode_alt":"","code_decimal":"🔀","name":"twisted rightwards arrows","shortname":":twisted_rightwards_arrows:","category":"symbols","emoji_order":"2048","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"repeat":{"unicode":"1f501","unicode_alt":"","code_decimal":"🔁","name":"clockwise rightwards and leftwards open circle arrows","shortname":":repeat:","category":"symbols","emoji_order":"2049","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"repeat_one":{"unicode":"1f502","unicode_alt":"","code_decimal":"🔂","name":"clockwise rightwards and leftwards open circle arrows with circled one overlay","shortname":":repeat_one:","category":"symbols","emoji_order":"2050","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_forward":{"unicode":"25b6","unicode_alt":"25b6-fe0f","code_decimal":"▶","name":"black right-pointing triangle","shortname":":arrow_forward:","category":"symbols","emoji_order":"2051","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol","triangle"]},"fast_forward":{"unicode":"23e9","unicode_alt":"","code_decimal":"⏩","name":"black right-pointing double triangle","shortname":":fast_forward:","category":"symbols","emoji_order":"2052","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"track_next":{"unicode":"23ed","unicode_alt":"23ed-fe0f","code_decimal":"⏭","name":"black right-pointing double triangle with vertical bar","shortname":":track_next:","category":"symbols","emoji_order":"2053","aliases":[":next_track:"],"aliases_ascii":[],"keywords":["arrow","symbol"]},"play_pause":{"unicode":"23ef","unicode_alt":"23ef-fe0f","code_decimal":"⏯","name":"black right-pointing double triangle with double vertical bar","shortname":":play_pause:","category":"symbols","emoji_order":"2054","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_backward":{"unicode":"25c0","unicode_alt":"25c0-fe0f","code_decimal":"◀","name":"black left-pointing triangle","shortname":":arrow_backward:","category":"symbols","emoji_order":"2055","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol","triangle"]},"rewind":{"unicode":"23ea","unicode_alt":"","code_decimal":"⏪","name":"black left-pointing double triangle","shortname":":rewind:","category":"symbols","emoji_order":"2056","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"track_previous":{"unicode":"23ee","unicode_alt":"23ee-fe0f","code_decimal":"⏮","name":"black left-pointing double triangle with vertical bar","shortname":":track_previous:","category":"symbols","emoji_order":"2057","aliases":[":previous_track:"],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_up_small":{"unicode":"1f53c","unicode_alt":"","code_decimal":"🔼","name":"up-pointing small red triangle","shortname":":arrow_up_small:","category":"symbols","emoji_order":"2058","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol","triangle"]},"arrow_double_up":{"unicode":"23eb","unicode_alt":"","code_decimal":"⏫","name":"black up-pointing double triangle","shortname":":arrow_double_up:","category":"symbols","emoji_order":"2059","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"arrow_down_small":{"unicode":"1f53d","unicode_alt":"","code_decimal":"🔽","name":"down-pointing small red triangle","shortname":":arrow_down_small:","category":"symbols","emoji_order":"2060","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol","triangle"]},"arrow_double_down":{"unicode":"23ec","unicode_alt":"","code_decimal":"⏬","name":"black down-pointing double triangle","shortname":":arrow_double_down:","category":"symbols","emoji_order":"2061","aliases":[],"aliases_ascii":[],"keywords":["arrow","symbol"]},"pause_button":{"unicode":"23f8","unicode_alt":"23f8-fe0f","code_decimal":"⏸","name":"double vertical bar","shortname":":pause_button:","category":"symbols","emoji_order":"2062","aliases":[":double_vertical_bar:"],"aliases_ascii":[],"keywords":["symbol"]},"stop_button":{"unicode":"23f9","unicode_alt":"23f9-fe0f","code_decimal":"⏹","name":"black square for stop","shortname":":stop_button:","category":"symbols","emoji_order":"2063","aliases":[],"aliases_ascii":[],"keywords":["symbol","square"]},"record_button":{"unicode":"23fa","unicode_alt":"23fa-fe0f","code_decimal":"⏺","name":"black circle for record","shortname":":record_button:","category":"symbols","emoji_order":"2064","aliases":[],"aliases_ascii":[],"keywords":["symbol","circle"]},"eject":{"unicode":"23cf","unicode_alt":"23cf-fe0f","code_decimal":"⏏","name":"eject symbol","shortname":":eject:","category":"symbols","emoji_order":"2065","aliases":[":eject_symbol:"],"aliases_ascii":[],"keywords":[]},"cinema":{"unicode":"1f3a6","unicode_alt":"","code_decimal":"🎦","name":"cinema","shortname":":cinema:","category":"symbols","emoji_order":"2066","aliases":[],"aliases_ascii":[],"keywords":["symbol","camera","movie"]},"low_brightness":{"unicode":"1f505","unicode_alt":"","code_decimal":"🔅","name":"low brightness symbol","shortname":":low_brightness:","category":"symbols","emoji_order":"2067","aliases":[],"aliases_ascii":[],"keywords":["symbol","sun"]},"high_brightness":{"unicode":"1f506","unicode_alt":"","code_decimal":"🔆","name":"high brightness symbol","shortname":":high_brightness:","category":"symbols","emoji_order":"2068","aliases":[],"aliases_ascii":[],"keywords":["symbol","sun"]},"signal_strength":{"unicode":"1f4f6","unicode_alt":"","code_decimal":"📶","name":"antenna with bars","shortname":":signal_strength:","category":"symbols","emoji_order":"2069","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"vibration_mode":{"unicode":"1f4f3","unicode_alt":"","code_decimal":"📳","name":"vibration mode","shortname":":vibration_mode:","category":"symbols","emoji_order":"2070","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"mobile_phone_off":{"unicode":"1f4f4","unicode_alt":"","code_decimal":"📴","name":"mobile phone off","shortname":":mobile_phone_off:","category":"symbols","emoji_order":"2071","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"recycle":{"unicode":"267b","unicode_alt":"267b-fe0f","code_decimal":"♻","name":"black universal recycling symbol","shortname":":recycle:","category":"symbols","emoji_order":"2072","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"name_badge":{"unicode":"1f4db","unicode_alt":"","code_decimal":"📛","name":"name badge","shortname":":name_badge:","category":"symbols","emoji_order":"2073","aliases":[],"aliases_ascii":[],"keywords":["work"]},"fleur-de-lis":{"unicode":"269c","unicode_alt":"269c-fe0f","code_decimal":"⚜","name":"fleur-de-lis","shortname":":fleur-de-lis:","category":"symbols","emoji_order":"2074","aliases":[],"aliases_ascii":[],"keywords":["object","symbol"]},"beginner":{"unicode":"1f530","unicode_alt":"","code_decimal":"🔰","name":"japanese symbol for beginner","shortname":":beginner:","category":"symbols","emoji_order":"2075","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"trident":{"unicode":"1f531","unicode_alt":"","code_decimal":"🔱","name":"trident emblem","shortname":":trident:","category":"symbols","emoji_order":"2076","aliases":[],"aliases_ascii":[],"keywords":["object","symbol"]},"o":{"unicode":"2b55","unicode_alt":"2b55-fe0f","code_decimal":"⭕","name":"heavy large circle","shortname":":o:","category":"symbols","emoji_order":"2077","aliases":[],"aliases_ascii":[],"keywords":["symbol","circle"]},"white_check_mark":{"unicode":"2705","unicode_alt":"","code_decimal":"✅","name":"white heavy check mark","shortname":":white_check_mark:","category":"symbols","emoji_order":"2078","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"ballot_box_with_check":{"unicode":"2611","unicode_alt":"2611-fe0f","code_decimal":"☑","name":"ballot box with check","shortname":":ballot_box_with_check:","category":"symbols","emoji_order":"2079","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"heavy_check_mark":{"unicode":"2714","unicode_alt":"2714-fe0f","code_decimal":"✔","name":"heavy check mark","shortname":":heavy_check_mark:","category":"symbols","emoji_order":"2080","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"heavy_multiplication_x":{"unicode":"2716","unicode_alt":"2716-fe0f","code_decimal":"✖","name":"heavy multiplication x","shortname":":heavy_multiplication_x:","category":"symbols","emoji_order":"2081","aliases":[],"aliases_ascii":[],"keywords":["math","symbol"]},"x":{"unicode":"274c","unicode_alt":"","code_decimal":"❌","name":"cross mark","shortname":":x:","category":"symbols","emoji_order":"2082","aliases":[],"aliases_ascii":[],"keywords":["symbol","sol"]},"negative_squared_cross_mark":{"unicode":"274e","unicode_alt":"","code_decimal":"❎","name":"negative squared cross mark","shortname":":negative_squared_cross_mark:","category":"symbols","emoji_order":"2083","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"heavy_plus_sign":{"unicode":"2795","unicode_alt":"","code_decimal":"➕","name":"heavy plus sign","shortname":":heavy_plus_sign:","category":"symbols","emoji_order":"2084","aliases":[],"aliases_ascii":[],"keywords":["math","symbol"]},"heavy_minus_sign":{"unicode":"2796","unicode_alt":"","code_decimal":"➖","name":"heavy minus sign","shortname":":heavy_minus_sign:","category":"symbols","emoji_order":"2088","aliases":[],"aliases_ascii":[],"keywords":["math","symbol"]},"heavy_division_sign":{"unicode":"2797","unicode_alt":"","code_decimal":"➗","name":"heavy division sign","shortname":":heavy_division_sign:","category":"symbols","emoji_order":"2089","aliases":[],"aliases_ascii":[],"keywords":["math","symbol"]},"curly_loop":{"unicode":"27b0","unicode_alt":"","code_decimal":"➰","name":"curly loop","shortname":":curly_loop:","category":"symbols","emoji_order":"2090","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"loop":{"unicode":"27bf","unicode_alt":"","code_decimal":"➿","name":"double curly loop","shortname":":loop:","category":"symbols","emoji_order":"2091","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"part_alternation_mark":{"unicode":"303d","unicode_alt":"303d-fe0f","code_decimal":"〽","name":"part alternation mark","shortname":":part_alternation_mark:","category":"symbols","emoji_order":"2092","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"eight_spoked_asterisk":{"unicode":"2733","unicode_alt":"2733-fe0f","code_decimal":"✳","name":"eight spoked asterisk","shortname":":eight_spoked_asterisk:","category":"symbols","emoji_order":"2093","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"eight_pointed_black_star":{"unicode":"2734","unicode_alt":"2734-fe0f","code_decimal":"✴","name":"eight pointed black star","shortname":":eight_pointed_black_star:","category":"symbols","emoji_order":"2094","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"sparkle":{"unicode":"2747","unicode_alt":"2747-fe0f","code_decimal":"❇","name":"sparkle","shortname":":sparkle:","category":"symbols","emoji_order":"2095","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"bangbang":{"unicode":"203c","unicode_alt":"203c-fe0f","code_decimal":"‼","name":"double exclamation mark","shortname":":bangbang:","category":"symbols","emoji_order":"2096","aliases":[],"aliases_ascii":[],"keywords":["symbol","punctuation"]},"interrobang":{"unicode":"2049","unicode_alt":"2049-fe0f","code_decimal":"⁉","name":"exclamation question mark","shortname":":interrobang:","category":"symbols","emoji_order":"2097","aliases":[],"aliases_ascii":[],"keywords":["symbol","punctuation"]},"question":{"unicode":"2753","unicode_alt":"","code_decimal":"❓","name":"black question mark ornament","shortname":":question:","category":"symbols","emoji_order":"2098","aliases":[],"aliases_ascii":[],"keywords":["symbol","punctuation","wth"]},"grey_question":{"unicode":"2754","unicode_alt":"","code_decimal":"❔","name":"white question mark ornament","shortname":":grey_question:","category":"symbols","emoji_order":"2099","aliases":[],"aliases_ascii":[],"keywords":["symbol","punctuation"]},"grey_exclamation":{"unicode":"2755","unicode_alt":"","code_decimal":"❕","name":"white exclamation mark ornament","shortname":":grey_exclamation:","category":"symbols","emoji_order":"2100","aliases":[],"aliases_ascii":[],"keywords":["symbol","punctuation"]},"exclamation":{"unicode":"2757","unicode_alt":"2757-fe0f","code_decimal":"❗","name":"heavy exclamation mark symbol","shortname":":exclamation:","category":"symbols","emoji_order":"2101","aliases":[],"aliases_ascii":[],"keywords":["symbol","punctuation"]},"wavy_dash":{"unicode":"3030","unicode_alt":"3030-fe0f","code_decimal":"〰","name":"wavy dash","shortname":":wavy_dash:","category":"symbols","emoji_order":"2102","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"copyright":{"unicode":"00a9","unicode_alt":"00a9-fe0f","code_decimal":"©","name":"copyright sign","shortname":":copyright:","category":"symbols","emoji_order":"2103","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"registered":{"unicode":"00ae","unicode_alt":"00ae-fe0f","code_decimal":"®","name":"registered sign","shortname":":registered:","category":"symbols","emoji_order":"2104","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"tm":{"unicode":"2122","unicode_alt":"2122-fe0f","code_decimal":"™","name":"trade mark sign","shortname":":tm:","category":"symbols","emoji_order":"2105","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"hash":{"unicode":"0023-20e3","unicode_alt":"0023-fe0f-20e3","code_decimal":"#⃣","name":"keycap number sign","shortname":":hash:","category":"symbols","emoji_order":"2106","aliases":[],"aliases_ascii":[],"keywords":["number","symbol"]},"asterisk":{"unicode":"002a-20e3","unicode_alt":"002a-fe0f-20e3","code_decimal":"*⃣","name":"keycap asterisk","shortname":":asterisk:","category":"symbols","emoji_order":"2107","aliases":[":keycap_asterisk:"],"aliases_ascii":[],"keywords":["symbol"]},"zero":{"unicode":"0030-20e3","unicode_alt":"0030-fe0f-20e3","code_decimal":"0⃣","name":"keycap digit zero","shortname":":zero:","category":"symbols","emoji_order":"2108","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"one":{"unicode":"0031-20e3","unicode_alt":"0031-fe0f-20e3","code_decimal":"1⃣","name":"keycap digit one","shortname":":one:","category":"symbols","emoji_order":"2109","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"two":{"unicode":"0032-20e3","unicode_alt":"0032-fe0f-20e3","code_decimal":"2⃣","name":"keycap digit two","shortname":":two:","category":"symbols","emoji_order":"2110","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"three":{"unicode":"0033-20e3","unicode_alt":"0033-fe0f-20e3","code_decimal":"3⃣","name":"keycap digit three","shortname":":three:","category":"symbols","emoji_order":"2111","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"four":{"unicode":"0034-20e3","unicode_alt":"0034-fe0f-20e3","code_decimal":"4⃣","name":"keycap digit four","shortname":":four:","category":"symbols","emoji_order":"2112","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"five":{"unicode":"0035-20e3","unicode_alt":"0035-fe0f-20e3","code_decimal":"5⃣","name":"keycap digit five","shortname":":five:","category":"symbols","emoji_order":"2113","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"six":{"unicode":"0036-20e3","unicode_alt":"0036-fe0f-20e3","code_decimal":"6⃣","name":"keycap digit six","shortname":":six:","category":"symbols","emoji_order":"2114","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"seven":{"unicode":"0037-20e3","unicode_alt":"0037-fe0f-20e3","code_decimal":"7⃣","name":"keycap digit seven","shortname":":seven:","category":"symbols","emoji_order":"2115","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"eight":{"unicode":"0038-20e3","unicode_alt":"0038-fe0f-20e3","code_decimal":"8⃣","name":"keycap digit eight","shortname":":eight:","category":"symbols","emoji_order":"2116","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"nine":{"unicode":"0039-20e3","unicode_alt":"0039-fe0f-20e3","code_decimal":"9⃣","name":"keycap digit nine","shortname":":nine:","category":"symbols","emoji_order":"2117","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"keycap_ten":{"unicode":"1f51f","unicode_alt":"","code_decimal":"🔟","name":"keycap ten","shortname":":keycap_ten:","category":"symbols","emoji_order":"2118","aliases":[],"aliases_ascii":[],"keywords":["number","math","symbol"]},"100":{"unicode":"1f4af","unicode_alt":"","code_decimal":"💯","name":"hundred points symbol","shortname":":100:","category":"symbols","emoji_order":"2119","aliases":[],"aliases_ascii":[],"keywords":["symbol","wow","win","perfect","parties"]},"capital_abcd":{"unicode":"1f520","unicode_alt":"","code_decimal":"🔠","name":"input symbol for latin capital letters","shortname":":capital_abcd:","category":"symbols","emoji_order":"2120","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"abcd":{"unicode":"1f521","unicode_alt":"","code_decimal":"🔡","name":"input symbol for latin small letters","shortname":":abcd:","category":"symbols","emoji_order":"2121","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"1234":{"unicode":"1f522","unicode_alt":"","code_decimal":"🔢","name":"input symbol for numbers","shortname":":1234:","category":"symbols","emoji_order":"2122","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"symbols":{"unicode":"1f523","unicode_alt":"","code_decimal":"🔣","name":"input symbol for symbols","shortname":":symbols:","category":"symbols","emoji_order":"2123","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"abc":{"unicode":"1f524","unicode_alt":"","code_decimal":"🔤","name":"input symbol for latin letters","shortname":":abc:","category":"symbols","emoji_order":"2124","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"a":{"unicode":"1f170","unicode_alt":"","code_decimal":"🅰","name":"negative squared latin capital letter a","shortname":":a:","category":"symbols","emoji_order":"2125","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"ab":{"unicode":"1f18e","unicode_alt":"","code_decimal":"🆎","name":"negative squared ab","shortname":":ab:","category":"symbols","emoji_order":"2126","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"b":{"unicode":"1f171","unicode_alt":"","code_decimal":"🅱","name":"negative squared latin capital letter b","shortname":":b:","category":"symbols","emoji_order":"2127","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"cl":{"unicode":"1f191","unicode_alt":"","code_decimal":"🆑","name":"squared cl","shortname":":cl:","category":"symbols","emoji_order":"2128","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"cool":{"unicode":"1f192","unicode_alt":"","code_decimal":"🆒","name":"squared cool","shortname":":cool:","category":"symbols","emoji_order":"2129","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"free":{"unicode":"1f193","unicode_alt":"","code_decimal":"🆓","name":"squared free","shortname":":free:","category":"symbols","emoji_order":"2130","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"information_source":{"unicode":"2139","unicode_alt":"2139-fe0f","code_decimal":"ℹ","name":"information source","shortname":":information_source:","category":"symbols","emoji_order":"2131","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"id":{"unicode":"1f194","unicode_alt":"","code_decimal":"🆔","name":"squared id","shortname":":id:","category":"symbols","emoji_order":"2132","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"m":{"unicode":"24c2","unicode_alt":"24c2-fe0f","code_decimal":"Ⓜ","name":"circled latin capital letter m","shortname":":m:","category":"symbols","emoji_order":"2133","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"new":{"unicode":"1f195","unicode_alt":"","code_decimal":"🆕","name":"squared new","shortname":":new:","category":"symbols","emoji_order":"2134","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"ng":{"unicode":"1f196","unicode_alt":"","code_decimal":"🆖","name":"squared ng","shortname":":ng:","category":"symbols","emoji_order":"2135","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"o2":{"unicode":"1f17e","unicode_alt":"","code_decimal":"🅾","name":"negative squared latin capital letter o","shortname":":o2:","category":"symbols","emoji_order":"2136","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"ok":{"unicode":"1f197","unicode_alt":"","code_decimal":"🆗","name":"squared ok","shortname":":ok:","category":"symbols","emoji_order":"2137","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"parking":{"unicode":"1f17f","unicode_alt":"1f17f-fe0f","code_decimal":"🅿","name":"negative squared latin capital letter p","shortname":":parking:","category":"symbols","emoji_order":"2138","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"sos":{"unicode":"1f198","unicode_alt":"","code_decimal":"🆘","name":"squared sos","shortname":":sos:","category":"symbols","emoji_order":"2139","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"up":{"unicode":"1f199","unicode_alt":"","code_decimal":"🆙","name":"squared up with exclamation mark","shortname":":up:","category":"symbols","emoji_order":"2140","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"vs":{"unicode":"1f19a","unicode_alt":"","code_decimal":"🆚","name":"squared vs","shortname":":vs:","category":"symbols","emoji_order":"2141","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"koko":{"unicode":"1f201","unicode_alt":"","code_decimal":"🈁","name":"squared katakana koko","shortname":":koko:","category":"symbols","emoji_order":"2142","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"sa":{"unicode":"1f202","unicode_alt":"1f202-fe0f","code_decimal":"🈂","name":"squared katakana sa","shortname":":sa:","category":"symbols","emoji_order":"2143","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u6708":{"unicode":"1f237","unicode_alt":"1f237-fe0f","code_decimal":"🈷","name":"squared cjk unified ideograph-6708","shortname":":u6708:","category":"symbols","emoji_order":"2144","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u6709":{"unicode":"1f236","unicode_alt":"","code_decimal":"🈶","name":"squared cjk unified ideograph-6709","shortname":":u6709:","category":"symbols","emoji_order":"2145","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u6307":{"unicode":"1f22f","unicode_alt":"1f22f-fe0f","code_decimal":"🈯","name":"squared cjk unified ideograph-6307","shortname":":u6307:","category":"symbols","emoji_order":"2146","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"ideograph_advantage":{"unicode":"1f250","unicode_alt":"","code_decimal":"🉐","name":"circled ideograph advantage","shortname":":ideograph_advantage:","category":"symbols","emoji_order":"2147","aliases":[],"aliases_ascii":[],"keywords":["japan","symbol"]},"u5272":{"unicode":"1f239","unicode_alt":"","code_decimal":"🈹","name":"squared cjk unified ideograph-5272","shortname":":u5272:","category":"symbols","emoji_order":"2148","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u7121":{"unicode":"1f21a","unicode_alt":"1f21a-fe0f","code_decimal":"🈚","name":"squared cjk unified ideograph-7121","shortname":":u7121:","category":"symbols","emoji_order":"2149","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u7981":{"unicode":"1f232","unicode_alt":"","code_decimal":"🈲","name":"squared cjk unified ideograph-7981","shortname":":u7981:","category":"symbols","emoji_order":"2150","aliases":[],"aliases_ascii":[],"keywords":["japan","symbol"]},"accept":{"unicode":"1f251","unicode_alt":"","code_decimal":"🉑","name":"circled ideograph accept","shortname":":accept:","category":"symbols","emoji_order":"2151","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u7533":{"unicode":"1f238","unicode_alt":"","code_decimal":"🈸","name":"squared cjk unified ideograph-7533","shortname":":u7533:","category":"symbols","emoji_order":"2152","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u5408":{"unicode":"1f234","unicode_alt":"","code_decimal":"🈴","name":"squared cjk unified ideograph-5408","shortname":":u5408:","category":"symbols","emoji_order":"2153","aliases":[],"aliases_ascii":[],"keywords":["japan","symbol"]},"u7a7a":{"unicode":"1f233","unicode_alt":"","code_decimal":"🈳","name":"squared cjk unified ideograph-7a7a","shortname":":u7a7a:","category":"symbols","emoji_order":"2154","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"congratulations":{"unicode":"3297","unicode_alt":"3297-fe0f","code_decimal":"㊗","name":"circled ideograph congratulation","shortname":":congratulations:","category":"symbols","emoji_order":"2155","aliases":[],"aliases_ascii":[],"keywords":["japan","symbol"]},"secret":{"unicode":"3299","unicode_alt":"3299-fe0f","code_decimal":"㊙","name":"circled ideograph secret","shortname":":secret:","category":"symbols","emoji_order":"2156","aliases":[],"aliases_ascii":[],"keywords":["japan","symbol"]},"u55b6":{"unicode":"1f23a","unicode_alt":"","code_decimal":"🈺","name":"squared cjk unified ideograph-55b6","shortname":":u55b6:","category":"symbols","emoji_order":"2157","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"u6e80":{"unicode":"1f235","unicode_alt":"","code_decimal":"🈵","name":"squared cjk unified ideograph-6e80","shortname":":u6e80:","category":"symbols","emoji_order":"2158","aliases":[],"aliases_ascii":[],"keywords":["japan","symbol"]},"black_small_square":{"unicode":"25aa","unicode_alt":"25aa-fe0f","code_decimal":"▪","name":"black small square","shortname":":black_small_square:","category":"symbols","emoji_order":"2159","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"white_small_square":{"unicode":"25ab","unicode_alt":"25ab-fe0f","code_decimal":"▫","name":"white small square","shortname":":white_small_square:","category":"symbols","emoji_order":"2160","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"white_medium_square":{"unicode":"25fb","unicode_alt":"25fb-fe0f","code_decimal":"◻","name":"white medium square","shortname":":white_medium_square:","category":"symbols","emoji_order":"2161","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"black_medium_square":{"unicode":"25fc","unicode_alt":"25fc-fe0f","code_decimal":"◼","name":"black medium square","shortname":":black_medium_square:","category":"symbols","emoji_order":"2162","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"white_medium_small_square":{"unicode":"25fd","unicode_alt":"25fd-fe0f","code_decimal":"◽","name":"white medium small square","shortname":":white_medium_small_square:","category":"symbols","emoji_order":"2163","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"black_medium_small_square":{"unicode":"25fe","unicode_alt":"25fe-fe0f","code_decimal":"◾","name":"black medium small square","shortname":":black_medium_small_square:","category":"symbols","emoji_order":"2164","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"black_large_square":{"unicode":"2b1b","unicode_alt":"2b1b-fe0f","code_decimal":"⬛","name":"black large square","shortname":":black_large_square:","category":"symbols","emoji_order":"2165","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"white_large_square":{"unicode":"2b1c","unicode_alt":"2b1c-fe0f","code_decimal":"⬜","name":"white large square","shortname":":white_large_square:","category":"symbols","emoji_order":"2166","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"large_orange_diamond":{"unicode":"1f536","unicode_alt":"","code_decimal":"🔶","name":"large orange diamond","shortname":":large_orange_diamond:","category":"symbols","emoji_order":"2167","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol"]},"large_blue_diamond":{"unicode":"1f537","unicode_alt":"","code_decimal":"🔷","name":"large blue diamond","shortname":":large_blue_diamond:","category":"symbols","emoji_order":"2168","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol"]},"small_orange_diamond":{"unicode":"1f538","unicode_alt":"","code_decimal":"🔸","name":"small orange diamond","shortname":":small_orange_diamond:","category":"symbols","emoji_order":"2169","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol"]},"small_blue_diamond":{"unicode":"1f539","unicode_alt":"","code_decimal":"🔹","name":"small blue diamond","shortname":":small_blue_diamond:","category":"symbols","emoji_order":"2170","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol"]},"small_red_triangle":{"unicode":"1f53a","unicode_alt":"","code_decimal":"🔺","name":"up-pointing red triangle","shortname":":small_red_triangle:","category":"symbols","emoji_order":"2171","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","triangle"]},"small_red_triangle_down":{"unicode":"1f53b","unicode_alt":"","code_decimal":"🔻","name":"down-pointing red triangle","shortname":":small_red_triangle_down:","category":"symbols","emoji_order":"2172","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","triangle"]},"diamond_shape_with_a_dot_inside":{"unicode":"1f4a0","unicode_alt":"","code_decimal":"💠","name":"diamond shape with a dot inside","shortname":":diamond_shape_with_a_dot_inside:","category":"symbols","emoji_order":"2173","aliases":[],"aliases_ascii":[],"keywords":["symbol"]},"radio_button":{"unicode":"1f518","unicode_alt":"","code_decimal":"🔘","name":"radio button","shortname":":radio_button:","category":"symbols","emoji_order":"2174","aliases":[],"aliases_ascii":[],"keywords":["symbol","circle"]},"black_square_button":{"unicode":"1f532","unicode_alt":"","code_decimal":"🔲","name":"black square button","shortname":":black_square_button:","category":"symbols","emoji_order":"2175","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"white_square_button":{"unicode":"1f533","unicode_alt":"","code_decimal":"🔳","name":"white square button","shortname":":white_square_button:","category":"symbols","emoji_order":"2176","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","square"]},"white_circle":{"unicode":"26aa","unicode_alt":"26aa-fe0f","code_decimal":"⚪","name":"white circle","shortname":":white_circle:","category":"symbols","emoji_order":"2177","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","circle"]},"black_circle":{"unicode":"26ab","unicode_alt":"26ab-fe0f","code_decimal":"⚫","name":"black circle","shortname":":black_circle:","category":"symbols","emoji_order":"2178","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","circle"]},"red_circle":{"unicode":"1f534","unicode_alt":"","code_decimal":"🔴","name":"red circle","shortname":":red_circle:","category":"symbols","emoji_order":"2179","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","circle"]},"blue_circle":{"unicode":"1f535","unicode_alt":"","code_decimal":"🔵","name":"blue circle","shortname":":blue_circle:","category":"symbols","emoji_order":"2180","aliases":[],"aliases_ascii":[],"keywords":["shapes","symbol","circle"]},"checkered_flag":{"unicode":"1f3c1","unicode_alt":"","code_decimal":"🏁","name":"chequered flag","shortname":":checkered_flag:","category":"travel","emoji_order":"2181","aliases":[],"aliases_ascii":[],"keywords":["object"]},"triangular_flag_on_post":{"unicode":"1f6a9","unicode_alt":"","code_decimal":"🚩","name":"triangular flag on post","shortname":":triangular_flag_on_post:","category":"objects","emoji_order":"2182","aliases":[],"aliases_ascii":[],"keywords":["object"]},"crossed_flags":{"unicode":"1f38c","unicode_alt":"","code_decimal":"🎌","name":"crossed flags","shortname":":crossed_flags:","category":"objects","emoji_order":"2183","aliases":[],"aliases_ascii":[],"keywords":["object","japan"]},"flag_black":{"unicode":"1f3f4","unicode_alt":"","code_decimal":"🏴","name":"waving black flag","shortname":":flag_black:","category":"objects","emoji_order":"2184","aliases":[":waving_black_flag:"],"aliases_ascii":[],"keywords":["object"]},"flag_white":{"unicode":"1f3f3","unicode_alt":"1f3f3-fe0f","code_decimal":"🏳","name":"waving white flag","shortname":":flag_white:","category":"objects","emoji_order":"2185","aliases":[":waving_white_flag:"],"aliases_ascii":[],"keywords":["object"]},"rainbow_flag":{"unicode":"1f3f3-1f308","unicode_alt":"","code_decimal":"🏳🌈","name":"rainbow_flag","shortname":":rainbow_flag:","category":"objects","emoji_order":"2186","aliases":[":gay_pride_flag:"],"aliases_ascii":[],"keywords":[]},"flag_ac":{"unicode":"1f1e6-1f1e8","unicode_alt":"","code_decimal":"🇦🇨","name":"ascension","shortname":":flag_ac:","category":"flags","emoji_order":"2187","aliases":[":ac:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ad":{"unicode":"1f1e6-1f1e9","unicode_alt":"","code_decimal":"🇦🇩","name":"andorra","shortname":":flag_ad:","category":"flags","emoji_order":"2188","aliases":[":ad:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ae":{"unicode":"1f1e6-1f1ea","unicode_alt":"","code_decimal":"🇦🇪","name":"the united arab emirates","shortname":":flag_ae:","category":"flags","emoji_order":"2189","aliases":[":ae:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_af":{"unicode":"1f1e6-1f1eb","unicode_alt":"","code_decimal":"🇦🇫","name":"afghanistan","shortname":":flag_af:","category":"flags","emoji_order":"2190","aliases":[":af:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ag":{"unicode":"1f1e6-1f1ec","unicode_alt":"","code_decimal":"🇦🇬","name":"antigua and barbuda","shortname":":flag_ag:","category":"flags","emoji_order":"2191","aliases":[":ag:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ai":{"unicode":"1f1e6-1f1ee","unicode_alt":"","code_decimal":"🇦🇮","name":"anguilla","shortname":":flag_ai:","category":"flags","emoji_order":"2192","aliases":[":ai:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_al":{"unicode":"1f1e6-1f1f1","unicode_alt":"","code_decimal":"🇦🇱","name":"albania","shortname":":flag_al:","category":"flags","emoji_order":"2193","aliases":[":al:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_am":{"unicode":"1f1e6-1f1f2","unicode_alt":"","code_decimal":"🇦🇲","name":"armenia","shortname":":flag_am:","category":"flags","emoji_order":"2194","aliases":[":am:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ao":{"unicode":"1f1e6-1f1f4","unicode_alt":"","code_decimal":"🇦🇴","name":"angola","shortname":":flag_ao:","category":"flags","emoji_order":"2195","aliases":[":ao:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_aq":{"unicode":"1f1e6-1f1f6","unicode_alt":"","code_decimal":"🇦🇶","name":"antarctica","shortname":":flag_aq:","category":"flags","emoji_order":"2196","aliases":[":aq:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ar":{"unicode":"1f1e6-1f1f7","unicode_alt":"","code_decimal":"🇦🇷","name":"argentina","shortname":":flag_ar:","category":"flags","emoji_order":"2197","aliases":[":ar:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_as":{"unicode":"1f1e6-1f1f8","unicode_alt":"","code_decimal":"🇦🇸","name":"american samoa","shortname":":flag_as:","category":"flags","emoji_order":"2198","aliases":[":as:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_at":{"unicode":"1f1e6-1f1f9","unicode_alt":"","code_decimal":"🇦🇹","name":"austria","shortname":":flag_at:","category":"flags","emoji_order":"2199","aliases":[":at:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_au":{"unicode":"1f1e6-1f1fa","unicode_alt":"","code_decimal":"🇦🇺","name":"australia","shortname":":flag_au:","category":"flags","emoji_order":"2200","aliases":[":au:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_aw":{"unicode":"1f1e6-1f1fc","unicode_alt":"","code_decimal":"🇦🇼","name":"aruba","shortname":":flag_aw:","category":"flags","emoji_order":"2201","aliases":[":aw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ax":{"unicode":"1f1e6-1f1fd","unicode_alt":"","code_decimal":"🇦🇽","name":"\u00e5land islands","shortname":":flag_ax:","category":"flags","emoji_order":"2202","aliases":[":ax:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_az":{"unicode":"1f1e6-1f1ff","unicode_alt":"","code_decimal":"🇦🇿","name":"azerbaijan","shortname":":flag_az:","category":"flags","emoji_order":"2203","aliases":[":az:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ba":{"unicode":"1f1e7-1f1e6","unicode_alt":"","code_decimal":"🇧🇦","name":"bosnia and herzegovina","shortname":":flag_ba:","category":"flags","emoji_order":"2204","aliases":[":ba:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bb":{"unicode":"1f1e7-1f1e7","unicode_alt":"","code_decimal":"🇧🇧","name":"barbados","shortname":":flag_bb:","category":"flags","emoji_order":"2205","aliases":[":bb:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bd":{"unicode":"1f1e7-1f1e9","unicode_alt":"","code_decimal":"🇧🇩","name":"bangladesh","shortname":":flag_bd:","category":"flags","emoji_order":"2206","aliases":[":bd:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_be":{"unicode":"1f1e7-1f1ea","unicode_alt":"","code_decimal":"🇧🇪","name":"belgium","shortname":":flag_be:","category":"flags","emoji_order":"2207","aliases":[":be:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bf":{"unicode":"1f1e7-1f1eb","unicode_alt":"","code_decimal":"🇧🇫","name":"burkina faso","shortname":":flag_bf:","category":"flags","emoji_order":"2208","aliases":[":bf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bg":{"unicode":"1f1e7-1f1ec","unicode_alt":"","code_decimal":"🇧🇬","name":"bulgaria","shortname":":flag_bg:","category":"flags","emoji_order":"2209","aliases":[":bg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bh":{"unicode":"1f1e7-1f1ed","unicode_alt":"","code_decimal":"🇧🇭","name":"bahrain","shortname":":flag_bh:","category":"flags","emoji_order":"2210","aliases":[":bh:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bi":{"unicode":"1f1e7-1f1ee","unicode_alt":"","code_decimal":"🇧🇮","name":"burundi","shortname":":flag_bi:","category":"flags","emoji_order":"2211","aliases":[":bi:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bj":{"unicode":"1f1e7-1f1ef","unicode_alt":"","code_decimal":"🇧🇯","name":"benin","shortname":":flag_bj:","category":"flags","emoji_order":"2212","aliases":[":bj:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bl":{"unicode":"1f1e7-1f1f1","unicode_alt":"","code_decimal":"🇧🇱","name":"saint barth\u00e9lemy","shortname":":flag_bl:","category":"flags","emoji_order":"2213","aliases":[":bl:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bm":{"unicode":"1f1e7-1f1f2","unicode_alt":"","code_decimal":"🇧🇲","name":"bermuda","shortname":":flag_bm:","category":"flags","emoji_order":"2214","aliases":[":bm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bn":{"unicode":"1f1e7-1f1f3","unicode_alt":"","code_decimal":"🇧🇳","name":"brunei","shortname":":flag_bn:","category":"flags","emoji_order":"2215","aliases":[":bn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bo":{"unicode":"1f1e7-1f1f4","unicode_alt":"","code_decimal":"🇧🇴","name":"bolivia","shortname":":flag_bo:","category":"flags","emoji_order":"2216","aliases":[":bo:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bq":{"unicode":"1f1e7-1f1f6","unicode_alt":"","code_decimal":"🇧🇶","name":"caribbean netherlands","shortname":":flag_bq:","category":"flags","emoji_order":"2217","aliases":[":bq:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_br":{"unicode":"1f1e7-1f1f7","unicode_alt":"","code_decimal":"🇧🇷","name":"brazil","shortname":":flag_br:","category":"flags","emoji_order":"2218","aliases":[":br:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bs":{"unicode":"1f1e7-1f1f8","unicode_alt":"","code_decimal":"🇧🇸","name":"the bahamas","shortname":":flag_bs:","category":"flags","emoji_order":"2219","aliases":[":bs:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bt":{"unicode":"1f1e7-1f1f9","unicode_alt":"","code_decimal":"🇧🇹","name":"bhutan","shortname":":flag_bt:","category":"flags","emoji_order":"2220","aliases":[":bt:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bv":{"unicode":"1f1e7-1f1fb","unicode_alt":"","code_decimal":"🇧🇻","name":"bouvet island","shortname":":flag_bv:","category":"flags","emoji_order":"2221","aliases":[":bv:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bw":{"unicode":"1f1e7-1f1fc","unicode_alt":"","code_decimal":"🇧🇼","name":"botswana","shortname":":flag_bw:","category":"flags","emoji_order":"2222","aliases":[":bw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_by":{"unicode":"1f1e7-1f1fe","unicode_alt":"","code_decimal":"🇧🇾","name":"belarus","shortname":":flag_by:","category":"flags","emoji_order":"2223","aliases":[":by:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_bz":{"unicode":"1f1e7-1f1ff","unicode_alt":"","code_decimal":"🇧🇿","name":"belize","shortname":":flag_bz:","category":"flags","emoji_order":"2224","aliases":[":bz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ca":{"unicode":"1f1e8-1f1e6","unicode_alt":"","code_decimal":"🇨🇦","name":"canada","shortname":":flag_ca:","category":"flags","emoji_order":"2225","aliases":[":ca:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cc":{"unicode":"1f1e8-1f1e8","unicode_alt":"","code_decimal":"🇨🇨","name":"cocos (keeling) islands","shortname":":flag_cc:","category":"flags","emoji_order":"2226","aliases":[":cc:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cd":{"unicode":"1f1e8-1f1e9","unicode_alt":"","code_decimal":"🇨🇩","name":"the democratic republic of the congo","shortname":":flag_cd:","category":"flags","emoji_order":"2227","aliases":[":congo:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cf":{"unicode":"1f1e8-1f1eb","unicode_alt":"","code_decimal":"🇨🇫","name":"central african republic","shortname":":flag_cf:","category":"flags","emoji_order":"2228","aliases":[":cf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cg":{"unicode":"1f1e8-1f1ec","unicode_alt":"","code_decimal":"🇨🇬","name":"the republic of the congo","shortname":":flag_cg:","category":"flags","emoji_order":"2229","aliases":[":cg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ch":{"unicode":"1f1e8-1f1ed","unicode_alt":"","code_decimal":"🇨🇭","name":"switzerland","shortname":":flag_ch:","category":"flags","emoji_order":"2230","aliases":[":ch:"],"aliases_ascii":[],"keywords":["country","neutral","flag"]},"flag_ci":{"unicode":"1f1e8-1f1ee","unicode_alt":"","code_decimal":"🇨🇮","name":"c\u00f4te d\u2019ivoire","shortname":":flag_ci:","category":"flags","emoji_order":"2231","aliases":[":ci:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ck":{"unicode":"1f1e8-1f1f0","unicode_alt":"","code_decimal":"🇨🇰","name":"cook islands","shortname":":flag_ck:","category":"flags","emoji_order":"2232","aliases":[":ck:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cl":{"unicode":"1f1e8-1f1f1","unicode_alt":"","code_decimal":"🇨🇱","name":"chile","shortname":":flag_cl:","category":"flags","emoji_order":"2233","aliases":[":chile:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cm":{"unicode":"1f1e8-1f1f2","unicode_alt":"","code_decimal":"🇨🇲","name":"cameroon","shortname":":flag_cm:","category":"flags","emoji_order":"2234","aliases":[":cm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cn":{"unicode":"1f1e8-1f1f3","unicode_alt":"","code_decimal":"🇨🇳","name":"china","shortname":":flag_cn:","category":"flags","emoji_order":"2235","aliases":[":cn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_co":{"unicode":"1f1e8-1f1f4","unicode_alt":"","code_decimal":"🇨🇴","name":"colombia","shortname":":flag_co:","category":"flags","emoji_order":"2236","aliases":[":co:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cp":{"unicode":"1f1e8-1f1f5","unicode_alt":"","code_decimal":"🇨🇵","name":"clipperton island","shortname":":flag_cp:","category":"flags","emoji_order":"2237","aliases":[":cp:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cr":{"unicode":"1f1e8-1f1f7","unicode_alt":"","code_decimal":"🇨🇷","name":"costa rica","shortname":":flag_cr:","category":"flags","emoji_order":"2238","aliases":[":cr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cu":{"unicode":"1f1e8-1f1fa","unicode_alt":"","code_decimal":"🇨🇺","name":"cuba","shortname":":flag_cu:","category":"flags","emoji_order":"2239","aliases":[":cu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cv":{"unicode":"1f1e8-1f1fb","unicode_alt":"","code_decimal":"🇨🇻","name":"cape verde","shortname":":flag_cv:","category":"flags","emoji_order":"2240","aliases":[":cv:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cw":{"unicode":"1f1e8-1f1fc","unicode_alt":"","code_decimal":"🇨🇼","name":"cura\u00e7ao","shortname":":flag_cw:","category":"flags","emoji_order":"2241","aliases":[":cw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cx":{"unicode":"1f1e8-1f1fd","unicode_alt":"","code_decimal":"🇨🇽","name":"christmas island","shortname":":flag_cx:","category":"flags","emoji_order":"2242","aliases":[":cx:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cy":{"unicode":"1f1e8-1f1fe","unicode_alt":"","code_decimal":"🇨🇾","name":"cyprus","shortname":":flag_cy:","category":"flags","emoji_order":"2243","aliases":[":cy:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_cz":{"unicode":"1f1e8-1f1ff","unicode_alt":"","code_decimal":"🇨🇿","name":"the czech republic","shortname":":flag_cz:","category":"flags","emoji_order":"2244","aliases":[":cz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_de":{"unicode":"1f1e9-1f1ea","unicode_alt":"","code_decimal":"🇩🇪","name":"germany","shortname":":flag_de:","category":"flags","emoji_order":"2245","aliases":[":de:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_dg":{"unicode":"1f1e9-1f1ec","unicode_alt":"","code_decimal":"🇩🇬","name":"diego garcia","shortname":":flag_dg:","category":"flags","emoji_order":"2246","aliases":[":dg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_dj":{"unicode":"1f1e9-1f1ef","unicode_alt":"","code_decimal":"🇩🇯","name":"djibouti","shortname":":flag_dj:","category":"flags","emoji_order":"2247","aliases":[":dj:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_dk":{"unicode":"1f1e9-1f1f0","unicode_alt":"","code_decimal":"🇩🇰","name":"denmark","shortname":":flag_dk:","category":"flags","emoji_order":"2248","aliases":[":dk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_dm":{"unicode":"1f1e9-1f1f2","unicode_alt":"","code_decimal":"🇩🇲","name":"dominica","shortname":":flag_dm:","category":"flags","emoji_order":"2249","aliases":[":dm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_do":{"unicode":"1f1e9-1f1f4","unicode_alt":"","code_decimal":"🇩🇴","name":"the dominican republic","shortname":":flag_do:","category":"flags","emoji_order":"2250","aliases":[":do:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_dz":{"unicode":"1f1e9-1f1ff","unicode_alt":"","code_decimal":"🇩🇿","name":"algeria","shortname":":flag_dz:","category":"flags","emoji_order":"2251","aliases":[":dz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ea":{"unicode":"1f1ea-1f1e6","unicode_alt":"","code_decimal":"🇪🇦","name":"ceuta, melilla","shortname":":flag_ea:","category":"flags","emoji_order":"2252","aliases":[":ea:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ec":{"unicode":"1f1ea-1f1e8","unicode_alt":"","code_decimal":"🇪🇨","name":"ecuador","shortname":":flag_ec:","category":"flags","emoji_order":"2253","aliases":[":ec:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ee":{"unicode":"1f1ea-1f1ea","unicode_alt":"","code_decimal":"🇪🇪","name":"estonia","shortname":":flag_ee:","category":"flags","emoji_order":"2254","aliases":[":ee:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_eg":{"unicode":"1f1ea-1f1ec","unicode_alt":"","code_decimal":"🇪🇬","name":"egypt","shortname":":flag_eg:","category":"flags","emoji_order":"2255","aliases":[":eg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_eh":{"unicode":"1f1ea-1f1ed","unicode_alt":"","code_decimal":"🇪🇭","name":"western sahara","shortname":":flag_eh:","category":"flags","emoji_order":"2256","aliases":[":eh:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_er":{"unicode":"1f1ea-1f1f7","unicode_alt":"","code_decimal":"🇪🇷","name":"eritrea","shortname":":flag_er:","category":"flags","emoji_order":"2257","aliases":[":er:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_es":{"unicode":"1f1ea-1f1f8","unicode_alt":"","code_decimal":"🇪🇸","name":"spain","shortname":":flag_es:","category":"flags","emoji_order":"2258","aliases":[":es:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_et":{"unicode":"1f1ea-1f1f9","unicode_alt":"","code_decimal":"🇪🇹","name":"ethiopia","shortname":":flag_et:","category":"flags","emoji_order":"2259","aliases":[":et:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_eu":{"unicode":"1f1ea-1f1fa","unicode_alt":"","code_decimal":"🇪🇺","name":"european union","shortname":":flag_eu:","category":"flags","emoji_order":"2260","aliases":[":eu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_fi":{"unicode":"1f1eb-1f1ee","unicode_alt":"","code_decimal":"🇫🇮","name":"finland","shortname":":flag_fi:","category":"flags","emoji_order":"2261","aliases":[":fi:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_fj":{"unicode":"1f1eb-1f1ef","unicode_alt":"","code_decimal":"🇫🇯","name":"fiji","shortname":":flag_fj:","category":"flags","emoji_order":"2262","aliases":[":fj:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_fk":{"unicode":"1f1eb-1f1f0","unicode_alt":"","code_decimal":"🇫🇰","name":"falkland islands","shortname":":flag_fk:","category":"flags","emoji_order":"2263","aliases":[":fk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_fm":{"unicode":"1f1eb-1f1f2","unicode_alt":"","code_decimal":"🇫🇲","name":"micronesia","shortname":":flag_fm:","category":"flags","emoji_order":"2264","aliases":[":fm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_fo":{"unicode":"1f1eb-1f1f4","unicode_alt":"","code_decimal":"🇫🇴","name":"faroe islands","shortname":":flag_fo:","category":"flags","emoji_order":"2265","aliases":[":fo:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_fr":{"unicode":"1f1eb-1f1f7","unicode_alt":"","code_decimal":"🇫🇷","name":"france","shortname":":flag_fr:","category":"flags","emoji_order":"2266","aliases":[":fr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ga":{"unicode":"1f1ec-1f1e6","unicode_alt":"","code_decimal":"🇬🇦","name":"gabon","shortname":":flag_ga:","category":"flags","emoji_order":"2267","aliases":[":ga:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gb":{"unicode":"1f1ec-1f1e7","unicode_alt":"","code_decimal":"🇬🇧","name":"great britain","shortname":":flag_gb:","category":"flags","emoji_order":"2268","aliases":[":gb:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gd":{"unicode":"1f1ec-1f1e9","unicode_alt":"","code_decimal":"🇬🇩","name":"grenada","shortname":":flag_gd:","category":"flags","emoji_order":"2269","aliases":[":gd:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ge":{"unicode":"1f1ec-1f1ea","unicode_alt":"","code_decimal":"🇬🇪","name":"georgia","shortname":":flag_ge:","category":"flags","emoji_order":"2270","aliases":[":ge:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gf":{"unicode":"1f1ec-1f1eb","unicode_alt":"","code_decimal":"🇬🇫","name":"french guiana","shortname":":flag_gf:","category":"flags","emoji_order":"2271","aliases":[":gf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gg":{"unicode":"1f1ec-1f1ec","unicode_alt":"","code_decimal":"🇬🇬","name":"guernsey","shortname":":flag_gg:","category":"flags","emoji_order":"2272","aliases":[":gg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gh":{"unicode":"1f1ec-1f1ed","unicode_alt":"","code_decimal":"🇬🇭","name":"ghana","shortname":":flag_gh:","category":"flags","emoji_order":"2273","aliases":[":gh:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gi":{"unicode":"1f1ec-1f1ee","unicode_alt":"","code_decimal":"🇬🇮","name":"gibraltar","shortname":":flag_gi:","category":"flags","emoji_order":"2274","aliases":[":gi:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gl":{"unicode":"1f1ec-1f1f1","unicode_alt":"","code_decimal":"🇬🇱","name":"greenland","shortname":":flag_gl:","category":"flags","emoji_order":"2275","aliases":[":gl:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gm":{"unicode":"1f1ec-1f1f2","unicode_alt":"","code_decimal":"🇬🇲","name":"the gambia","shortname":":flag_gm:","category":"flags","emoji_order":"2276","aliases":[":gm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gn":{"unicode":"1f1ec-1f1f3","unicode_alt":"","code_decimal":"🇬🇳","name":"guinea","shortname":":flag_gn:","category":"flags","emoji_order":"2277","aliases":[":gn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gp":{"unicode":"1f1ec-1f1f5","unicode_alt":"","code_decimal":"🇬🇵","name":"guadeloupe","shortname":":flag_gp:","category":"flags","emoji_order":"2278","aliases":[":gp:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gq":{"unicode":"1f1ec-1f1f6","unicode_alt":"","code_decimal":"🇬🇶","name":"equatorial guinea","shortname":":flag_gq:","category":"flags","emoji_order":"2279","aliases":[":gq:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gr":{"unicode":"1f1ec-1f1f7","unicode_alt":"","code_decimal":"🇬🇷","name":"greece","shortname":":flag_gr:","category":"flags","emoji_order":"2280","aliases":[":gr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gs":{"unicode":"1f1ec-1f1f8","unicode_alt":"","code_decimal":"🇬🇸","name":"south georgia","shortname":":flag_gs:","category":"flags","emoji_order":"2281","aliases":[":gs:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gt":{"unicode":"1f1ec-1f1f9","unicode_alt":"","code_decimal":"🇬🇹","name":"guatemala","shortname":":flag_gt:","category":"flags","emoji_order":"2282","aliases":[":gt:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gu":{"unicode":"1f1ec-1f1fa","unicode_alt":"","code_decimal":"🇬🇺","name":"guam","shortname":":flag_gu:","category":"flags","emoji_order":"2283","aliases":[":gu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gw":{"unicode":"1f1ec-1f1fc","unicode_alt":"","code_decimal":"🇬🇼","name":"guinea-bissau","shortname":":flag_gw:","category":"flags","emoji_order":"2284","aliases":[":gw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_gy":{"unicode":"1f1ec-1f1fe","unicode_alt":"","code_decimal":"🇬🇾","name":"guyana","shortname":":flag_gy:","category":"flags","emoji_order":"2285","aliases":[":gy:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_hk":{"unicode":"1f1ed-1f1f0","unicode_alt":"","code_decimal":"🇭🇰","name":"hong kong","shortname":":flag_hk:","category":"flags","emoji_order":"2286","aliases":[":hk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_hm":{"unicode":"1f1ed-1f1f2","unicode_alt":"","code_decimal":"🇭🇲","name":"heard island and mcdonald islands","shortname":":flag_hm:","category":"flags","emoji_order":"2287","aliases":[":hm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_hn":{"unicode":"1f1ed-1f1f3","unicode_alt":"","code_decimal":"🇭🇳","name":"honduras","shortname":":flag_hn:","category":"flags","emoji_order":"2288","aliases":[":hn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_hr":{"unicode":"1f1ed-1f1f7","unicode_alt":"","code_decimal":"🇭🇷","name":"croatia","shortname":":flag_hr:","category":"flags","emoji_order":"2289","aliases":[":hr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ht":{"unicode":"1f1ed-1f1f9","unicode_alt":"","code_decimal":"🇭🇹","name":"haiti","shortname":":flag_ht:","category":"flags","emoji_order":"2290","aliases":[":ht:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_hu":{"unicode":"1f1ed-1f1fa","unicode_alt":"","code_decimal":"🇭🇺","name":"hungary","shortname":":flag_hu:","category":"flags","emoji_order":"2291","aliases":[":hu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ic":{"unicode":"1f1ee-1f1e8","unicode_alt":"","code_decimal":"🇮🇨","name":"canary islands","shortname":":flag_ic:","category":"flags","emoji_order":"2292","aliases":[":ic:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_id":{"unicode":"1f1ee-1f1e9","unicode_alt":"","code_decimal":"🇮🇩","name":"indonesia","shortname":":flag_id:","category":"flags","emoji_order":"2293","aliases":[":indonesia:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ie":{"unicode":"1f1ee-1f1ea","unicode_alt":"","code_decimal":"🇮🇪","name":"ireland","shortname":":flag_ie:","category":"flags","emoji_order":"2294","aliases":[":ie:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_il":{"unicode":"1f1ee-1f1f1","unicode_alt":"","code_decimal":"🇮🇱","name":"israel","shortname":":flag_il:","category":"flags","emoji_order":"2295","aliases":[":il:"],"aliases_ascii":[],"keywords":["jew","country","flag"]},"flag_im":{"unicode":"1f1ee-1f1f2","unicode_alt":"","code_decimal":"🇮🇲","name":"isle of man","shortname":":flag_im:","category":"flags","emoji_order":"2296","aliases":[":im:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_in":{"unicode":"1f1ee-1f1f3","unicode_alt":"","code_decimal":"🇮🇳","name":"india","shortname":":flag_in:","category":"flags","emoji_order":"2297","aliases":[":in:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_io":{"unicode":"1f1ee-1f1f4","unicode_alt":"","code_decimal":"🇮🇴","name":"british indian ocean territory","shortname":":flag_io:","category":"flags","emoji_order":"2298","aliases":[":io:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_iq":{"unicode":"1f1ee-1f1f6","unicode_alt":"","code_decimal":"🇮🇶","name":"iraq","shortname":":flag_iq:","category":"flags","emoji_order":"2299","aliases":[":iq:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ir":{"unicode":"1f1ee-1f1f7","unicode_alt":"","code_decimal":"🇮🇷","name":"iran","shortname":":flag_ir:","category":"flags","emoji_order":"2300","aliases":[":ir:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_is":{"unicode":"1f1ee-1f1f8","unicode_alt":"","code_decimal":"🇮🇸","name":"iceland","shortname":":flag_is:","category":"flags","emoji_order":"2301","aliases":[":is:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_it":{"unicode":"1f1ee-1f1f9","unicode_alt":"","code_decimal":"🇮🇹","name":"italy","shortname":":flag_it:","category":"flags","emoji_order":"2302","aliases":[":it:"],"aliases_ascii":[],"keywords":["italian","country","flag"]},"flag_je":{"unicode":"1f1ef-1f1ea","unicode_alt":"","code_decimal":"🇯🇪","name":"jersey","shortname":":flag_je:","category":"flags","emoji_order":"2303","aliases":[":je:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_jm":{"unicode":"1f1ef-1f1f2","unicode_alt":"","code_decimal":"🇯🇲","name":"jamaica","shortname":":flag_jm:","category":"flags","emoji_order":"2304","aliases":[":jm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_jo":{"unicode":"1f1ef-1f1f4","unicode_alt":"","code_decimal":"🇯🇴","name":"jordan","shortname":":flag_jo:","category":"flags","emoji_order":"2305","aliases":[":jo:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_jp":{"unicode":"1f1ef-1f1f5","unicode_alt":"","code_decimal":"🇯🇵","name":"japan","shortname":":flag_jp:","category":"flags","emoji_order":"2306","aliases":[":jp:"],"aliases_ascii":[],"keywords":["japan","country","flag"]},"flag_ke":{"unicode":"1f1f0-1f1ea","unicode_alt":"","code_decimal":"🇰🇪","name":"kenya","shortname":":flag_ke:","category":"flags","emoji_order":"2307","aliases":[":ke:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_kg":{"unicode":"1f1f0-1f1ec","unicode_alt":"","code_decimal":"🇰🇬","name":"kyrgyzstan","shortname":":flag_kg:","category":"flags","emoji_order":"2308","aliases":[":kg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_kh":{"unicode":"1f1f0-1f1ed","unicode_alt":"","code_decimal":"🇰🇭","name":"cambodia","shortname":":flag_kh:","category":"flags","emoji_order":"2309","aliases":[":kh:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ki":{"unicode":"1f1f0-1f1ee","unicode_alt":"","code_decimal":"🇰🇮","name":"kiribati","shortname":":flag_ki:","category":"flags","emoji_order":"2310","aliases":[":ki:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_km":{"unicode":"1f1f0-1f1f2","unicode_alt":"","code_decimal":"🇰🇲","name":"the comoros","shortname":":flag_km:","category":"flags","emoji_order":"2311","aliases":[":km:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_kn":{"unicode":"1f1f0-1f1f3","unicode_alt":"","code_decimal":"🇰🇳","name":"saint kitts and nevis","shortname":":flag_kn:","category":"flags","emoji_order":"2312","aliases":[":kn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_kp":{"unicode":"1f1f0-1f1f5","unicode_alt":"","code_decimal":"🇰🇵","name":"north korea","shortname":":flag_kp:","category":"flags","emoji_order":"2313","aliases":[":kp:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_kr":{"unicode":"1f1f0-1f1f7","unicode_alt":"","code_decimal":"🇰🇷","name":"korea","shortname":":flag_kr:","category":"flags","emoji_order":"2314","aliases":[":kr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_kw":{"unicode":"1f1f0-1f1fc","unicode_alt":"","code_decimal":"🇰🇼","name":"kuwait","shortname":":flag_kw:","category":"flags","emoji_order":"2315","aliases":[":kw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ky":{"unicode":"1f1f0-1f1fe","unicode_alt":"","code_decimal":"🇰🇾","name":"cayman islands","shortname":":flag_ky:","category":"flags","emoji_order":"2316","aliases":[":ky:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_kz":{"unicode":"1f1f0-1f1ff","unicode_alt":"","code_decimal":"🇰🇿","name":"kazakhstan","shortname":":flag_kz:","category":"flags","emoji_order":"2317","aliases":[":kz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_la":{"unicode":"1f1f1-1f1e6","unicode_alt":"","code_decimal":"🇱🇦","name":"laos","shortname":":flag_la:","category":"flags","emoji_order":"2318","aliases":[":la:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_lb":{"unicode":"1f1f1-1f1e7","unicode_alt":"","code_decimal":"🇱🇧","name":"lebanon","shortname":":flag_lb:","category":"flags","emoji_order":"2319","aliases":[":lb:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_lc":{"unicode":"1f1f1-1f1e8","unicode_alt":"","code_decimal":"🇱🇨","name":"saint lucia","shortname":":flag_lc:","category":"flags","emoji_order":"2320","aliases":[":lc:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_li":{"unicode":"1f1f1-1f1ee","unicode_alt":"","code_decimal":"🇱🇮","name":"liechtenstein","shortname":":flag_li:","category":"flags","emoji_order":"2321","aliases":[":li:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_lk":{"unicode":"1f1f1-1f1f0","unicode_alt":"","code_decimal":"🇱🇰","name":"sri lanka","shortname":":flag_lk:","category":"flags","emoji_order":"2322","aliases":[":lk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_lr":{"unicode":"1f1f1-1f1f7","unicode_alt":"","code_decimal":"🇱🇷","name":"liberia","shortname":":flag_lr:","category":"flags","emoji_order":"2323","aliases":[":lr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ls":{"unicode":"1f1f1-1f1f8","unicode_alt":"","code_decimal":"🇱🇸","name":"lesotho","shortname":":flag_ls:","category":"flags","emoji_order":"2324","aliases":[":ls:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_lt":{"unicode":"1f1f1-1f1f9","unicode_alt":"","code_decimal":"🇱🇹","name":"lithuania","shortname":":flag_lt:","category":"flags","emoji_order":"2325","aliases":[":lt:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_lu":{"unicode":"1f1f1-1f1fa","unicode_alt":"","code_decimal":"🇱🇺","name":"luxembourg","shortname":":flag_lu:","category":"flags","emoji_order":"2326","aliases":[":lu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_lv":{"unicode":"1f1f1-1f1fb","unicode_alt":"","code_decimal":"🇱🇻","name":"latvia","shortname":":flag_lv:","category":"flags","emoji_order":"2327","aliases":[":lv:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ly":{"unicode":"1f1f1-1f1fe","unicode_alt":"","code_decimal":"🇱🇾","name":"libya","shortname":":flag_ly:","category":"flags","emoji_order":"2328","aliases":[":ly:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ma":{"unicode":"1f1f2-1f1e6","unicode_alt":"","code_decimal":"🇲🇦","name":"morocco","shortname":":flag_ma:","category":"flags","emoji_order":"2329","aliases":[":ma:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mc":{"unicode":"1f1f2-1f1e8","unicode_alt":"","code_decimal":"🇲🇨","name":"monaco","shortname":":flag_mc:","category":"flags","emoji_order":"2330","aliases":[":mc:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_md":{"unicode":"1f1f2-1f1e9","unicode_alt":"","code_decimal":"🇲🇩","name":"moldova","shortname":":flag_md:","category":"flags","emoji_order":"2331","aliases":[":md:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_me":{"unicode":"1f1f2-1f1ea","unicode_alt":"","code_decimal":"🇲🇪","name":"montenegro","shortname":":flag_me:","category":"flags","emoji_order":"2332","aliases":[":me:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mf":{"unicode":"1f1f2-1f1eb","unicode_alt":"","code_decimal":"🇲🇫","name":"saint martin","shortname":":flag_mf:","category":"flags","emoji_order":"2333","aliases":[":mf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mg":{"unicode":"1f1f2-1f1ec","unicode_alt":"","code_decimal":"🇲🇬","name":"madagascar","shortname":":flag_mg:","category":"flags","emoji_order":"2334","aliases":[":mg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mh":{"unicode":"1f1f2-1f1ed","unicode_alt":"","code_decimal":"🇲🇭","name":"the marshall islands","shortname":":flag_mh:","category":"flags","emoji_order":"2335","aliases":[":mh:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mk":{"unicode":"1f1f2-1f1f0","unicode_alt":"","code_decimal":"🇲🇰","name":"macedonia","shortname":":flag_mk:","category":"flags","emoji_order":"2336","aliases":[":mk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ml":{"unicode":"1f1f2-1f1f1","unicode_alt":"","code_decimal":"🇲🇱","name":"mali","shortname":":flag_ml:","category":"flags","emoji_order":"2337","aliases":[":ml:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mm":{"unicode":"1f1f2-1f1f2","unicode_alt":"","code_decimal":"🇲🇲","name":"myanmar","shortname":":flag_mm:","category":"flags","emoji_order":"2338","aliases":[":mm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mn":{"unicode":"1f1f2-1f1f3","unicode_alt":"","code_decimal":"🇲🇳","name":"mongolia","shortname":":flag_mn:","category":"flags","emoji_order":"2339","aliases":[":mn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mo":{"unicode":"1f1f2-1f1f4","unicode_alt":"","code_decimal":"🇲🇴","name":"macau","shortname":":flag_mo:","category":"flags","emoji_order":"2340","aliases":[":mo:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mp":{"unicode":"1f1f2-1f1f5","unicode_alt":"","code_decimal":"🇲🇵","name":"northern mariana islands","shortname":":flag_mp:","category":"flags","emoji_order":"2341","aliases":[":mp:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mq":{"unicode":"1f1f2-1f1f6","unicode_alt":"","code_decimal":"🇲🇶","name":"martinique","shortname":":flag_mq:","category":"flags","emoji_order":"2342","aliases":[":mq:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mr":{"unicode":"1f1f2-1f1f7","unicode_alt":"","code_decimal":"🇲🇷","name":"mauritania","shortname":":flag_mr:","category":"flags","emoji_order":"2343","aliases":[":mr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ms":{"unicode":"1f1f2-1f1f8","unicode_alt":"","code_decimal":"🇲🇸","name":"montserrat","shortname":":flag_ms:","category":"flags","emoji_order":"2344","aliases":[":ms:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mt":{"unicode":"1f1f2-1f1f9","unicode_alt":"","code_decimal":"🇲🇹","name":"malta","shortname":":flag_mt:","category":"flags","emoji_order":"2345","aliases":[":mt:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mu":{"unicode":"1f1f2-1f1fa","unicode_alt":"","code_decimal":"🇲🇺","name":"mauritius","shortname":":flag_mu:","category":"flags","emoji_order":"2346","aliases":[":mu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mv":{"unicode":"1f1f2-1f1fb","unicode_alt":"","code_decimal":"🇲🇻","name":"maldives","shortname":":flag_mv:","category":"flags","emoji_order":"2347","aliases":[":mv:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mw":{"unicode":"1f1f2-1f1fc","unicode_alt":"","code_decimal":"🇲🇼","name":"malawi","shortname":":flag_mw:","category":"flags","emoji_order":"2348","aliases":[":mw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mx":{"unicode":"1f1f2-1f1fd","unicode_alt":"","code_decimal":"🇲🇽","name":"mexico","shortname":":flag_mx:","category":"flags","emoji_order":"2349","aliases":[":mx:"],"aliases_ascii":[],"keywords":["country","mexican","flag"]},"flag_my":{"unicode":"1f1f2-1f1fe","unicode_alt":"","code_decimal":"🇲🇾","name":"malaysia","shortname":":flag_my:","category":"flags","emoji_order":"2350","aliases":[":my:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_mz":{"unicode":"1f1f2-1f1ff","unicode_alt":"","code_decimal":"🇲🇿","name":"mozambique","shortname":":flag_mz:","category":"flags","emoji_order":"2351","aliases":[":mz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_na":{"unicode":"1f1f3-1f1e6","unicode_alt":"","code_decimal":"🇳🇦","name":"namibia","shortname":":flag_na:","category":"flags","emoji_order":"2352","aliases":[":na:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_nc":{"unicode":"1f1f3-1f1e8","unicode_alt":"","code_decimal":"🇳🇨","name":"new caledonia","shortname":":flag_nc:","category":"flags","emoji_order":"2353","aliases":[":nc:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ne":{"unicode":"1f1f3-1f1ea","unicode_alt":"","code_decimal":"🇳🇪","name":"niger","shortname":":flag_ne:","category":"flags","emoji_order":"2354","aliases":[":ne:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_nf":{"unicode":"1f1f3-1f1eb","unicode_alt":"","code_decimal":"🇳🇫","name":"norfolk island","shortname":":flag_nf:","category":"flags","emoji_order":"2355","aliases":[":nf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ng":{"unicode":"1f1f3-1f1ec","unicode_alt":"","code_decimal":"🇳🇬","name":"nigeria","shortname":":flag_ng:","category":"flags","emoji_order":"2356","aliases":[":nigeria:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ni":{"unicode":"1f1f3-1f1ee","unicode_alt":"","code_decimal":"🇳🇮","name":"nicaragua","shortname":":flag_ni:","category":"flags","emoji_order":"2357","aliases":[":ni:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_nl":{"unicode":"1f1f3-1f1f1","unicode_alt":"","code_decimal":"🇳🇱","name":"the netherlands","shortname":":flag_nl:","category":"flags","emoji_order":"2358","aliases":[":nl:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_no":{"unicode":"1f1f3-1f1f4","unicode_alt":"","code_decimal":"🇳🇴","name":"norway","shortname":":flag_no:","category":"flags","emoji_order":"2359","aliases":[":no:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_np":{"unicode":"1f1f3-1f1f5","unicode_alt":"","code_decimal":"🇳🇵","name":"nepal","shortname":":flag_np:","category":"flags","emoji_order":"2360","aliases":[":np:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_nr":{"unicode":"1f1f3-1f1f7","unicode_alt":"","code_decimal":"🇳🇷","name":"nauru","shortname":":flag_nr:","category":"flags","emoji_order":"2361","aliases":[":nr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_nu":{"unicode":"1f1f3-1f1fa","unicode_alt":"","code_decimal":"🇳🇺","name":"niue","shortname":":flag_nu:","category":"flags","emoji_order":"2362","aliases":[":nu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_nz":{"unicode":"1f1f3-1f1ff","unicode_alt":"","code_decimal":"🇳🇿","name":"new zealand","shortname":":flag_nz:","category":"flags","emoji_order":"2363","aliases":[":nz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_om":{"unicode":"1f1f4-1f1f2","unicode_alt":"","code_decimal":"🇴🇲","name":"oman","shortname":":flag_om:","category":"flags","emoji_order":"2364","aliases":[":om:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pa":{"unicode":"1f1f5-1f1e6","unicode_alt":"","code_decimal":"🇵🇦","name":"panama","shortname":":flag_pa:","category":"flags","emoji_order":"2365","aliases":[":pa:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pe":{"unicode":"1f1f5-1f1ea","unicode_alt":"","code_decimal":"🇵🇪","name":"peru","shortname":":flag_pe:","category":"flags","emoji_order":"2366","aliases":[":pe:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pf":{"unicode":"1f1f5-1f1eb","unicode_alt":"","code_decimal":"🇵🇫","name":"french polynesia","shortname":":flag_pf:","category":"flags","emoji_order":"2367","aliases":[":pf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pg":{"unicode":"1f1f5-1f1ec","unicode_alt":"","code_decimal":"🇵🇬","name":"papua new guinea","shortname":":flag_pg:","category":"flags","emoji_order":"2368","aliases":[":pg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ph":{"unicode":"1f1f5-1f1ed","unicode_alt":"","code_decimal":"🇵🇭","name":"the philippines","shortname":":flag_ph:","category":"flags","emoji_order":"2369","aliases":[":ph:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pk":{"unicode":"1f1f5-1f1f0","unicode_alt":"","code_decimal":"🇵🇰","name":"pakistan","shortname":":flag_pk:","category":"flags","emoji_order":"2370","aliases":[":pk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pl":{"unicode":"1f1f5-1f1f1","unicode_alt":"","code_decimal":"🇵🇱","name":"poland","shortname":":flag_pl:","category":"flags","emoji_order":"2371","aliases":[":pl:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pm":{"unicode":"1f1f5-1f1f2","unicode_alt":"","code_decimal":"🇵🇲","name":"saint pierre and miquelon","shortname":":flag_pm:","category":"flags","emoji_order":"2372","aliases":[":pm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pn":{"unicode":"1f1f5-1f1f3","unicode_alt":"","code_decimal":"🇵🇳","name":"pitcairn","shortname":":flag_pn:","category":"flags","emoji_order":"2373","aliases":[":pn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pr":{"unicode":"1f1f5-1f1f7","unicode_alt":"","code_decimal":"🇵🇷","name":"puerto rico","shortname":":flag_pr:","category":"flags","emoji_order":"2374","aliases":[":pr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ps":{"unicode":"1f1f5-1f1f8","unicode_alt":"","code_decimal":"🇵🇸","name":"palestinian authority","shortname":":flag_ps:","category":"flags","emoji_order":"2375","aliases":[":ps:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pt":{"unicode":"1f1f5-1f1f9","unicode_alt":"","code_decimal":"🇵🇹","name":"portugal","shortname":":flag_pt:","category":"flags","emoji_order":"2376","aliases":[":pt:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_pw":{"unicode":"1f1f5-1f1fc","unicode_alt":"","code_decimal":"🇵🇼","name":"palau","shortname":":flag_pw:","category":"flags","emoji_order":"2377","aliases":[":pw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_py":{"unicode":"1f1f5-1f1fe","unicode_alt":"","code_decimal":"🇵🇾","name":"paraguay","shortname":":flag_py:","category":"flags","emoji_order":"2378","aliases":[":py:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_qa":{"unicode":"1f1f6-1f1e6","unicode_alt":"","code_decimal":"🇶🇦","name":"qatar","shortname":":flag_qa:","category":"flags","emoji_order":"2379","aliases":[":qa:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_re":{"unicode":"1f1f7-1f1ea","unicode_alt":"","code_decimal":"🇷🇪","name":"r\u00e9union","shortname":":flag_re:","category":"flags","emoji_order":"2380","aliases":[":re:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ro":{"unicode":"1f1f7-1f1f4","unicode_alt":"","code_decimal":"🇷🇴","name":"romania","shortname":":flag_ro:","category":"flags","emoji_order":"2381","aliases":[":ro:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_rs":{"unicode":"1f1f7-1f1f8","unicode_alt":"","code_decimal":"🇷🇸","name":"serbia","shortname":":flag_rs:","category":"flags","emoji_order":"2382","aliases":[":rs:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ru":{"unicode":"1f1f7-1f1fa","unicode_alt":"","code_decimal":"🇷🇺","name":"russia","shortname":":flag_ru:","category":"flags","emoji_order":"2383","aliases":[":ru:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_rw":{"unicode":"1f1f7-1f1fc","unicode_alt":"","code_decimal":"🇷🇼","name":"rwanda","shortname":":flag_rw:","category":"flags","emoji_order":"2384","aliases":[":rw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sa":{"unicode":"1f1f8-1f1e6","unicode_alt":"","code_decimal":"🇸🇦","name":"saudi arabia","shortname":":flag_sa:","category":"flags","emoji_order":"2385","aliases":[":saudiarabia:",":saudi:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sb":{"unicode":"1f1f8-1f1e7","unicode_alt":"","code_decimal":"🇸🇧","name":"the solomon islands","shortname":":flag_sb:","category":"flags","emoji_order":"2386","aliases":[":sb:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sc":{"unicode":"1f1f8-1f1e8","unicode_alt":"","code_decimal":"🇸🇨","name":"the seychelles","shortname":":flag_sc:","category":"flags","emoji_order":"2387","aliases":[":sc:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sd":{"unicode":"1f1f8-1f1e9","unicode_alt":"","code_decimal":"🇸🇩","name":"sudan","shortname":":flag_sd:","category":"flags","emoji_order":"2388","aliases":[":sd:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_se":{"unicode":"1f1f8-1f1ea","unicode_alt":"","code_decimal":"🇸🇪","name":"sweden","shortname":":flag_se:","category":"flags","emoji_order":"2389","aliases":[":se:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sg":{"unicode":"1f1f8-1f1ec","unicode_alt":"","code_decimal":"🇸🇬","name":"singapore","shortname":":flag_sg:","category":"flags","emoji_order":"2390","aliases":[":sg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sh":{"unicode":"1f1f8-1f1ed","unicode_alt":"","code_decimal":"🇸🇭","name":"saint helena","shortname":":flag_sh:","category":"flags","emoji_order":"2391","aliases":[":sh:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_si":{"unicode":"1f1f8-1f1ee","unicode_alt":"","code_decimal":"🇸🇮","name":"slovenia","shortname":":flag_si:","category":"flags","emoji_order":"2392","aliases":[":si:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sj":{"unicode":"1f1f8-1f1ef","unicode_alt":"","code_decimal":"🇸🇯","name":"svalbard and jan mayen","shortname":":flag_sj:","category":"flags","emoji_order":"2393","aliases":[":sj:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sk":{"unicode":"1f1f8-1f1f0","unicode_alt":"","code_decimal":"🇸🇰","name":"slovakia","shortname":":flag_sk:","category":"flags","emoji_order":"2394","aliases":[":sk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sl":{"unicode":"1f1f8-1f1f1","unicode_alt":"","code_decimal":"🇸🇱","name":"sierra leone","shortname":":flag_sl:","category":"flags","emoji_order":"2395","aliases":[":sl:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sm":{"unicode":"1f1f8-1f1f2","unicode_alt":"","code_decimal":"🇸🇲","name":"san marino","shortname":":flag_sm:","category":"flags","emoji_order":"2396","aliases":[":sm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sn":{"unicode":"1f1f8-1f1f3","unicode_alt":"","code_decimal":"🇸🇳","name":"senegal","shortname":":flag_sn:","category":"flags","emoji_order":"2397","aliases":[":sn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_so":{"unicode":"1f1f8-1f1f4","unicode_alt":"","code_decimal":"🇸🇴","name":"somalia","shortname":":flag_so:","category":"flags","emoji_order":"2398","aliases":[":so:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sr":{"unicode":"1f1f8-1f1f7","unicode_alt":"","code_decimal":"🇸🇷","name":"suriname","shortname":":flag_sr:","category":"flags","emoji_order":"2399","aliases":[":sr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ss":{"unicode":"1f1f8-1f1f8","unicode_alt":"","code_decimal":"🇸🇸","name":"south sudan","shortname":":flag_ss:","category":"flags","emoji_order":"2400","aliases":[":ss:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_st":{"unicode":"1f1f8-1f1f9","unicode_alt":"","code_decimal":"🇸🇹","name":"s\u00e3o tom\u00e9 and pr\u00edncipe","shortname":":flag_st:","category":"flags","emoji_order":"2401","aliases":[":st:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sv":{"unicode":"1f1f8-1f1fb","unicode_alt":"","code_decimal":"🇸🇻","name":"el salvador","shortname":":flag_sv:","category":"flags","emoji_order":"2402","aliases":[":sv:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sx":{"unicode":"1f1f8-1f1fd","unicode_alt":"","code_decimal":"🇸🇽","name":"sint maarten","shortname":":flag_sx:","category":"flags","emoji_order":"2403","aliases":[":sx:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sy":{"unicode":"1f1f8-1f1fe","unicode_alt":"","code_decimal":"🇸🇾","name":"syria","shortname":":flag_sy:","category":"flags","emoji_order":"2404","aliases":[":sy:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_sz":{"unicode":"1f1f8-1f1ff","unicode_alt":"","code_decimal":"🇸🇿","name":"swaziland","shortname":":flag_sz:","category":"flags","emoji_order":"2405","aliases":[":sz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ta":{"unicode":"1f1f9-1f1e6","unicode_alt":"","code_decimal":"🇹🇦","name":"tristan da cunha","shortname":":flag_ta:","category":"flags","emoji_order":"2406","aliases":[":ta:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tc":{"unicode":"1f1f9-1f1e8","unicode_alt":"","code_decimal":"🇹🇨","name":"turks and caicos islands","shortname":":flag_tc:","category":"flags","emoji_order":"2407","aliases":[":tc:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_td":{"unicode":"1f1f9-1f1e9","unicode_alt":"","code_decimal":"🇹🇩","name":"chad","shortname":":flag_td:","category":"flags","emoji_order":"2408","aliases":[":td:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tf":{"unicode":"1f1f9-1f1eb","unicode_alt":"","code_decimal":"🇹🇫","name":"french southern territories","shortname":":flag_tf:","category":"flags","emoji_order":"2409","aliases":[":tf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tg":{"unicode":"1f1f9-1f1ec","unicode_alt":"","code_decimal":"🇹🇬","name":"togo","shortname":":flag_tg:","category":"flags","emoji_order":"2410","aliases":[":tg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_th":{"unicode":"1f1f9-1f1ed","unicode_alt":"","code_decimal":"🇹🇭","name":"thailand","shortname":":flag_th:","category":"flags","emoji_order":"2411","aliases":[":th:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tj":{"unicode":"1f1f9-1f1ef","unicode_alt":"","code_decimal":"🇹🇯","name":"tajikistan","shortname":":flag_tj:","category":"flags","emoji_order":"2412","aliases":[":tj:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tk":{"unicode":"1f1f9-1f1f0","unicode_alt":"","code_decimal":"🇹🇰","name":"tokelau","shortname":":flag_tk:","category":"flags","emoji_order":"2413","aliases":[":tk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tl":{"unicode":"1f1f9-1f1f1","unicode_alt":"","code_decimal":"🇹🇱","name":"timor-leste","shortname":":flag_tl:","category":"flags","emoji_order":"2414","aliases":[":tl:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tm":{"unicode":"1f1f9-1f1f2","unicode_alt":"","code_decimal":"🇹🇲","name":"turkmenistan","shortname":":flag_tm:","category":"flags","emoji_order":"2415","aliases":[":turkmenistan:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tn":{"unicode":"1f1f9-1f1f3","unicode_alt":"","code_decimal":"🇹🇳","name":"tunisia","shortname":":flag_tn:","category":"flags","emoji_order":"2416","aliases":[":tn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_to":{"unicode":"1f1f9-1f1f4","unicode_alt":"","code_decimal":"🇹🇴","name":"tonga","shortname":":flag_to:","category":"flags","emoji_order":"2417","aliases":[":to:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tr":{"unicode":"1f1f9-1f1f7","unicode_alt":"","code_decimal":"🇹🇷","name":"turkey","shortname":":flag_tr:","category":"flags","emoji_order":"2418","aliases":[":tr:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tt":{"unicode":"1f1f9-1f1f9","unicode_alt":"","code_decimal":"🇹🇹","name":"trinidad and tobago","shortname":":flag_tt:","category":"flags","emoji_order":"2419","aliases":[":tt:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tv":{"unicode":"1f1f9-1f1fb","unicode_alt":"","code_decimal":"🇹🇻","name":"tuvalu","shortname":":flag_tv:","category":"flags","emoji_order":"2420","aliases":[":tuvalu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tw":{"unicode":"1f1f9-1f1fc","unicode_alt":"","code_decimal":"🇹🇼","name":"the republic of china","shortname":":flag_tw:","category":"flags","emoji_order":"2421","aliases":[":tw:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_tz":{"unicode":"1f1f9-1f1ff","unicode_alt":"","code_decimal":"🇹🇿","name":"tanzania","shortname":":flag_tz:","category":"flags","emoji_order":"2422","aliases":[":tz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ua":{"unicode":"1f1fa-1f1e6","unicode_alt":"","code_decimal":"🇺🇦","name":"ukraine","shortname":":flag_ua:","category":"flags","emoji_order":"2423","aliases":[":ua:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ug":{"unicode":"1f1fa-1f1ec","unicode_alt":"","code_decimal":"🇺🇬","name":"uganda","shortname":":flag_ug:","category":"flags","emoji_order":"2424","aliases":[":ug:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_um":{"unicode":"1f1fa-1f1f2","unicode_alt":"","code_decimal":"🇺🇲","name":"united states minor outlying islands","shortname":":flag_um:","category":"flags","emoji_order":"2425","aliases":[":um:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_us":{"unicode":"1f1fa-1f1f8","unicode_alt":"","code_decimal":"🇺🇸","name":"united states","shortname":":flag_us:","category":"flags","emoji_order":"2427","aliases":[":us:"],"aliases_ascii":[],"keywords":["america","country","flag"]},"flag_uy":{"unicode":"1f1fa-1f1fe","unicode_alt":"","code_decimal":"🇺🇾","name":"uruguay","shortname":":flag_uy:","category":"flags","emoji_order":"2428","aliases":[":uy:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_uz":{"unicode":"1f1fa-1f1ff","unicode_alt":"","code_decimal":"🇺🇿","name":"uzbekistan","shortname":":flag_uz:","category":"flags","emoji_order":"2429","aliases":[":uz:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_va":{"unicode":"1f1fb-1f1e6","unicode_alt":"","code_decimal":"🇻🇦","name":"the vatican city","shortname":":flag_va:","category":"flags","emoji_order":"2430","aliases":[":va:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_vc":{"unicode":"1f1fb-1f1e8","unicode_alt":"","code_decimal":"🇻🇨","name":"saint vincent and the grenadines","shortname":":flag_vc:","category":"flags","emoji_order":"2431","aliases":[":vc:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ve":{"unicode":"1f1fb-1f1ea","unicode_alt":"","code_decimal":"🇻🇪","name":"venezuela","shortname":":flag_ve:","category":"flags","emoji_order":"2432","aliases":[":ve:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_vg":{"unicode":"1f1fb-1f1ec","unicode_alt":"","code_decimal":"🇻🇬","name":"british virgin islands","shortname":":flag_vg:","category":"flags","emoji_order":"2433","aliases":[":vg:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_vi":{"unicode":"1f1fb-1f1ee","unicode_alt":"","code_decimal":"🇻🇮","name":"u.s. virgin islands","shortname":":flag_vi:","category":"flags","emoji_order":"2434","aliases":[":vi:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_vn":{"unicode":"1f1fb-1f1f3","unicode_alt":"","code_decimal":"🇻🇳","name":"vietnam","shortname":":flag_vn:","category":"flags","emoji_order":"2435","aliases":[":vn:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_vu":{"unicode":"1f1fb-1f1fa","unicode_alt":"","code_decimal":"🇻🇺","name":"vanuatu","shortname":":flag_vu:","category":"flags","emoji_order":"2436","aliases":[":vu:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_wf":{"unicode":"1f1fc-1f1eb","unicode_alt":"","code_decimal":"🇼🇫","name":"wallis and futuna","shortname":":flag_wf:","category":"flags","emoji_order":"2437","aliases":[":wf:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ws":{"unicode":"1f1fc-1f1f8","unicode_alt":"","code_decimal":"🇼🇸","name":"samoa","shortname":":flag_ws:","category":"flags","emoji_order":"2438","aliases":[":ws:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_xk":{"unicode":"1f1fd-1f1f0","unicode_alt":"","code_decimal":"🇽🇰","name":"kosovo","shortname":":flag_xk:","category":"flags","emoji_order":"2439","aliases":[":xk:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_ye":{"unicode":"1f1fe-1f1ea","unicode_alt":"","code_decimal":"🇾🇪","name":"yemen","shortname":":flag_ye:","category":"flags","emoji_order":"2440","aliases":[":ye:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_yt":{"unicode":"1f1fe-1f1f9","unicode_alt":"","code_decimal":"🇾🇹","name":"mayotte","shortname":":flag_yt:","category":"flags","emoji_order":"2441","aliases":[":yt:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_za":{"unicode":"1f1ff-1f1e6","unicode_alt":"","code_decimal":"🇿🇦","name":"south africa","shortname":":flag_za:","category":"flags","emoji_order":"2442","aliases":[":za:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_zm":{"unicode":"1f1ff-1f1f2","unicode_alt":"","code_decimal":"🇿🇲","name":"zambia","shortname":":flag_zm:","category":"flags","emoji_order":"2443","aliases":[":zm:"],"aliases_ascii":[],"keywords":["country","flag"]},"flag_zw":{"unicode":"1f1ff-1f1fc","unicode_alt":"","code_decimal":"🇿🇼","name":"zimbabwe","shortname":":flag_zw:","category":"flags","emoji_order":"2444","aliases":[":zw:"],"aliases_ascii":[],"keywords":["country","flag"]},"regional_indicator_z":{"unicode":"1f1ff","unicode_alt":"","code_decimal":"🇿","name":"regional indicator symbol letter z","shortname":":regional_indicator_z:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_y":{"unicode":"1f1fe","unicode_alt":"","code_decimal":"🇾","name":"regional indicator symbol letter y","shortname":":regional_indicator_y:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_x":{"unicode":"1f1fd","unicode_alt":"","code_decimal":"🇽","name":"regional indicator symbol letter x","shortname":":regional_indicator_x:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_w":{"unicode":"1f1fc","unicode_alt":"","code_decimal":"🇼","name":"regional indicator symbol letter w","shortname":":regional_indicator_w:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_v":{"unicode":"1f1fb","unicode_alt":"","code_decimal":"🇻","name":"regional indicator symbol letter v","shortname":":regional_indicator_v:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_u":{"unicode":"1f1fa","unicode_alt":"","code_decimal":"🇺","name":"regional indicator symbol letter u","shortname":":regional_indicator_u:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_t":{"unicode":"1f1f9","unicode_alt":"","code_decimal":"🇹","name":"regional indicator symbol letter t","shortname":":regional_indicator_t:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_s":{"unicode":"1f1f8","unicode_alt":"","code_decimal":"🇸","name":"regional indicator symbol letter s","shortname":":regional_indicator_s:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_r":{"unicode":"1f1f7","unicode_alt":"","code_decimal":"🇷","name":"regional indicator symbol letter r","shortname":":regional_indicator_r:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_q":{"unicode":"1f1f6","unicode_alt":"","code_decimal":"🇶","name":"regional indicator symbol letter q","shortname":":regional_indicator_q:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_p":{"unicode":"1f1f5","unicode_alt":"","code_decimal":"🇵","name":"regional indicator symbol letter p","shortname":":regional_indicator_p:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_o":{"unicode":"1f1f4","unicode_alt":"","code_decimal":"🇴","name":"regional indicator symbol letter o","shortname":":regional_indicator_o:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_n":{"unicode":"1f1f3","unicode_alt":"","code_decimal":"🇳","name":"regional indicator symbol letter n","shortname":":regional_indicator_n:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_m":{"unicode":"1f1f2","unicode_alt":"","code_decimal":"🇲","name":"regional indicator symbol letter m","shortname":":regional_indicator_m:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_l":{"unicode":"1f1f1","unicode_alt":"","code_decimal":"🇱","name":"regional indicator symbol letter l","shortname":":regional_indicator_l:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_k":{"unicode":"1f1f0","unicode_alt":"","code_decimal":"🇰","name":"regional indicator symbol letter k","shortname":":regional_indicator_k:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_j":{"unicode":"1f1ef","unicode_alt":"","code_decimal":"🇯","name":"regional indicator symbol letter j","shortname":":regional_indicator_j:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_i":{"unicode":"1f1ee","unicode_alt":"","code_decimal":"🇮","name":"regional indicator symbol letter i","shortname":":regional_indicator_i:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_h":{"unicode":"1f1ed","unicode_alt":"","code_decimal":"🇭","name":"regional indicator symbol letter h","shortname":":regional_indicator_h:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_g":{"unicode":"1f1ec","unicode_alt":"","code_decimal":"🇬","name":"regional indicator symbol letter g","shortname":":regional_indicator_g:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_f":{"unicode":"1f1eb","unicode_alt":"","code_decimal":"🇫","name":"regional indicator symbol letter f","shortname":":regional_indicator_f:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_e":{"unicode":"1f1ea","unicode_alt":"","code_decimal":"🇪","name":"regional indicator symbol letter e","shortname":":regional_indicator_e:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_d":{"unicode":"1f1e9","unicode_alt":"","code_decimal":"🇩","name":"regional indicator symbol letter d","shortname":":regional_indicator_d:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_c":{"unicode":"1f1e8","unicode_alt":"","code_decimal":"🇨","name":"regional indicator symbol letter c","shortname":":regional_indicator_c:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_b":{"unicode":"1f1e7","unicode_alt":"","code_decimal":"🇧","name":"regional indicator symbol letter b","shortname":":regional_indicator_b:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]},"regional_indicator_a":{"unicode":"1f1e6","unicode_alt":"","code_decimal":"🇦","name":"regional indicator symbol letter a","shortname":":regional_indicator_a:","category":"regional","emoji_order":"12345","aliases":[],"aliases_ascii":[],"keywords":[]}} diff --git a/resources/fonts/EmojiOne/emojione-android.ttf b/resources/fonts/EmojiOne/emojione-android.ttf deleted file mode 100644 index 4cd640d0..00000000 --- a/resources/fonts/EmojiOne/emojione-android.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/LICENSE.txt b/resources/fonts/OpenSans/LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/resources/fonts/OpenSans/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 deleted file mode 100644 index fd79d43b..00000000 --- a/resources/fonts/OpenSans/OpenSans-Bold.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-BoldItalic.ttf b/resources/fonts/OpenSans/OpenSans-BoldItalic.ttf deleted file mode 100644 index 9bc80095..00000000 --- a/resources/fonts/OpenSans/OpenSans-BoldItalic.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-ExtraBold.ttf b/resources/fonts/OpenSans/OpenSans-ExtraBold.ttf deleted file mode 100644 index 21f6f84a..00000000 --- a/resources/fonts/OpenSans/OpenSans-ExtraBold.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf b/resources/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf deleted file mode 100644 index 31cb6883..00000000 --- a/resources/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-Italic.ttf b/resources/fonts/OpenSans/OpenSans-Italic.ttf deleted file mode 100644 index c90da48f..00000000 --- a/resources/fonts/OpenSans/OpenSans-Italic.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-Light.ttf b/resources/fonts/OpenSans/OpenSans-Light.ttf deleted file mode 100644 index 0d381897..00000000 --- a/resources/fonts/OpenSans/OpenSans-Light.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-LightItalic.ttf b/resources/fonts/OpenSans/OpenSans-LightItalic.ttf deleted file mode 100644 index 68299c4b..00000000 --- a/resources/fonts/OpenSans/OpenSans-LightItalic.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-Regular.ttf b/resources/fonts/OpenSans/OpenSans-Regular.ttf deleted file mode 100644 index db433349..00000000 --- a/resources/fonts/OpenSans/OpenSans-Regular.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-Semibold.ttf b/resources/fonts/OpenSans/OpenSans-Semibold.ttf deleted file mode 100644 index 1a7679e3..00000000 --- a/resources/fonts/OpenSans/OpenSans-Semibold.ttf +++ /dev/null Binary files differdiff --git a/resources/fonts/OpenSans/OpenSans-SemiboldItalic.ttf b/resources/fonts/OpenSans/OpenSans-SemiboldItalic.ttf deleted file mode 100644 index 59b6d16b..00000000 --- a/resources/fonts/OpenSans/OpenSans-SemiboldItalic.ttf +++ /dev/null Binary files differdiff --git a/resources/icons/ui/at-solid.svg b/resources/icons/ui/at-solid.svg new file mode 100644 index 00000000..8b72d6f8 --- /dev/null +++ b/resources/icons/ui/at-solid.svg @@ -0,0 +1 @@ +<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="at" class="svg-inline--fa fa-at fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C118.941 8 8 118.919 8 256c0 137.059 110.919 248 248 248 48.154 0 95.342-14.14 135.408-40.223 12.005-7.815 14.625-24.288 5.552-35.372l-10.177-12.433c-7.671-9.371-21.179-11.667-31.373-5.129C325.92 429.757 291.314 440 256 440c-101.458 0-184-82.542-184-184S154.542 72 256 72c100.139 0 184 57.619 184 160 0 38.786-21.093 79.742-58.17 83.693-17.349-.454-16.91-12.857-13.476-30.024l23.433-121.11C394.653 149.75 383.308 136 368.225 136h-44.981a13.518 13.518 0 0 0-13.432 11.993l-.01.092c-14.697-17.901-40.448-21.775-59.971-21.775-74.58 0-137.831 62.234-137.831 151.46 0 65.303 36.785 105.87 96 105.87 26.984 0 57.369-15.637 74.991-38.333 9.522 34.104 40.613 34.103 70.71 34.103C462.609 379.41 504 307.798 504 232 504 95.653 394.023 8 256 8zm-21.68 304.43c-22.249 0-36.07-15.623-36.07-40.771 0-44.993 30.779-72.729 58.63-72.729 22.292 0 35.601 15.241 35.601 40.77 0 45.061-33.875 72.73-58.161 72.73z"></path></svg> \ No newline at end of file diff --git a/resources/icons/ui/mail-reply.png b/resources/icons/ui/mail-reply.png new file mode 100644 index 00000000..a9d377d0 --- /dev/null +++ b/resources/icons/ui/mail-reply.png Binary files differdiff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index 81e440fe..63772bf9 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -2,42 +2,155 @@ <!DOCTYPE TS> <TS version="2.1" language="de"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cc" line="+125"/> - <source>Save File</source> - <translation>Datei speichern</translation> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation>Medienupload fehlgeschlagen. Bitte versuche es erneut.</translation> + </message> + <message> + <location line="+399"/> + <source>Failed to restore OLM account. Please login again.</source> + <translation>Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein.</translation> + </message> + <message> + <location line="+5"/> + <source>Failed to restore save data. Please login again.</source> + <translation>Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an.</translation> + </message> + <message> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation>Fehler beim Setup der Verschlüsselungsschlüssel. Servermeldung: %1 %2. Bitte versuche es später erneut.</translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> + <source>Please try to login again: %1</source> + <translation>Bitte melde dich erneut an: %1</translation> + </message> + <message> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Room creation failed: %1</source> + <translation>Raum konnte nicht erstellt werden: %1</translation> + </message> + <message> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Failed to leave room: %1</source> + <translation>Konnte den Raum nicht verlassen: %1</translation> </message> </context> <context> - <name>DateSeparator</name> + <name>CommunitiesListItem</name> + <message> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation>Alle Räume</translation> + </message> <message> - <location filename="../../src/timeline/TimelineView.cc" line="+54"/> - <source>Today</source> - <translation>Heute</translation> + <location line="+4"/> + <source>Favourite rooms</source> + <translation>Favoriten</translation> </message> <message> <location line="+2"/> - <source>Yesterday</source> - <translation>Gestern</translation> + <source>Low priority rooms</source> + <translation>Räume niedriger Priorität</translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation> (tag)</translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation> (community)</translation> </message> </context> <context> <name>EditModal</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+34"/> - <source>APPLY</source> - <translation>EINSETZEN</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> + <translation>Anwenden</translation> </message> <message> - <location line="+2"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location line="+1"/> + <source>Cancel</source> + <translation>Abbrechen</translation> </message> <message> - <location line="+9"/> + <location line="+10"/> <source>Name</source> - <translation>Titel</translation> + <translation>Name</translation> </message> <message> <location line="+2"/> @@ -46,25 +159,25 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cc" line="+111"/> - <source>Save File</source> - <translation>Datei speichern</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation>Verschlüsselt</translation> </message> </context> <context> - <name>ImageItem</name> + <name>InviteeItem</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cc" line="+229"/> - <source>Save image</source> - <translation>Bild speichern</translation> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation>Löschen</translation> </message> </context> <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cc" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> <translation>Matrix-ID</translation> </message> @@ -79,64 +192,112 @@ <translation>Passwort</translation> </message> <message> + <location line="+4"/> + <source>Device name</source> + <translation>Gerätename</translation> + </message> + <message> <location line="+19"/> <source>LOGIN</source> <translation>ANMELDEN</translation> </message> <message> - <location line="+128"/> - <source>Empty password</source> - <translation>Leeres Passwort</translation> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation>Automatische Erkennung fehlgeschlagen. Antwort war fehlerhaft.</translation> </message> -</context> -<context> - <name>MatrixClient</name> <message> - <location filename="../../src/MatrixClient.cc" line="+164"/> - <source>Wrong username or password</source> - <translation>Falscher Benutzername oder Passwort</translation> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation>Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage .well-known.</translation> </message> <message> - <location line="+5"/> - <source>Login endpoint was not found on the server</source> - <translation>Login-Endpunkt wurde auf dem Server nicht gefunden</translation> + <location line="+24"/> + <source>The required endpoints were not found. Possibly not a Matrix server.</source> + <translation>Benötigte Ansprechpunkte nicht auffindbar. Möglicherweise kein Matrixserver.</translation> </message> <message> <location line="+6"/> - <source>An unknown error occured. Please try again.</source> - <translation>Ein unbekannter Fehler trat auf. Bitte erneut versuchen.</translation> + <source>Received malformed response. Make sure the homeserver domain is valid.</source> + <translation>Erhaltene Antwort war fehlerhaft. Bitte Homeserverdomain prüfen.</translation> </message> <message> - <location line="+23"/> - <source>Malformed response. Possibly not a Matrix server</source> - <translation>Ungewöhnliche Antwort. Vielleicht kein Matrix-Server</translation> + <location line="+5"/> + <source>An unknown error occured. Make sure the homeserver domain is valid.</source> + <translation>Ein unbekannter Fehler ist aufgetreten. Bitte Homeserverdomain prüfen.</translation> + </message> + <message> + <location line="+60"/> + <source>Empty password</source> + <translation>Leeres Passwort</translation> </message> </context> <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+79"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> <source>Room members</source> <translation>Teilnehmerliste</translation> </message> <message> - <location line="+10"/> - <source>SHOW MORE</source> - <translation>MEHR ZEIGEN</translation> + <location line="+4"/> + <source>OK</source> + <translation>OK</translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation>gelöscht</translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation>Verschlüsselung aktiviert</translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation>Raumname wurde gändert auf: %1</translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation>Raumname wurde entfernt</translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation>Raumthema wurde geändert auf: %1</translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation>Raumthema wurde entfernt.</translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation>Unimplementiertes Event: </translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cc" line="+70"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> - <translation>Raum suchen...</translation> + <translation>Raum suchen…</translation> </message> </context> <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cc" line="+76"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> <translation>Benutzername</translation> </message> @@ -153,15 +314,15 @@ <message> <location line="+4"/> <source>Home Server</source> - <translation>Heimserver</translation> + <translation>Homeserver</translation> </message> <message> - <location line="+17"/> + <location line="+16"/> <source>REGISTER</source> <translation>REGISTRIEREN</translation> </message> <message> - <location line="+76"/> + <location line="+93"/> <source>Invalid username</source> <translation>Ungültiger Benutzername</translation> </message> @@ -178,23 +339,39 @@ <message> <location line="+2"/> <source>Invalid server name</source> - <translation>Ungültiger Server-Name</translation> + <translation>Ungültiger Servername</translation> + </message> +</context> +<context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation>Abmelden</translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation>keine Version gespeichert</translation> </message> </context> <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cc" line="+78"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Raum verlassen</translation> </message> <message> - <location line="+153"/> + <location line="+151"/> <source>Accept</source> - <translation>Akzeptieren </translation> + <translation>Akzeptieren</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> <translation>Ablehnen</translation> </message> @@ -202,7 +379,12 @@ <context> <name>SideBarActions</name> <message> - <location filename="../../src/SideBarActions.cc" line="+36"/> + <location filename="../../src/SideBarActions.cpp" line="+38"/> + <source>User settings</source> + <translation>Benutzereinstellungen</translation> + </message> + <message> + <location line="+7"/> <source>Create new room</source> <translation>Neuen Raum erstellen</translation> </message> @@ -211,16 +393,65 @@ <source>Join a room</source> <translation>Raum betreten</translation> </message> + <message> + <location line="+16"/> + <source>Start a new chat</source> + <translation>Neues Gespräch beginnen</translation> + </message> + <message> + <location line="+15"/> + <source>Room directory</source> + <translation>Raumverzeichnis</translation> + </message> +</context> +<context> + <name>StatusIndicator</name> + <message> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation>Fehlgeschlagen</translation> + </message> + <message> + <location line="+1"/> + <source>Sent</source> + <translation>Gesendet</translation> + </message> + <message> + <location line="+1"/> + <source>Received</source> + <translation>Empfangen</translation> + </message> + <message> + <location line="+1"/> + <source>Read</source> + <translation>Gelesen</translation> + </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cc" line="+445"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> + <source>Send a file</source> + <translation>Versende Datei</translation> + </message> + <message> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> - <translation>Schreibe eine Nachricht...</translation> + <translation>Schreibe eine Nachricht…</translation> + </message> + <message> + <location line="+31"/> + <source>Send a message</source> + <translation>Versende eine Nachricht</translation> </message> <message> - <location line="+108"/> + <location line="+8"/> + <source>Emoji</source> + <translation>Emoji</translation> + </message> + <message> + <location line="+85"/> <source>Select a file</source> <translation>Datei auswählen</translation> </message> @@ -229,11 +460,220 @@ <source>All Files (*)</source> <translation>Alle Dateien (*)</translation> </message> + <message> + <location filename="../../src/TextInputWidget.h" line="-5"/> + <source>Connection lost. Nheko is trying to re-connect...</source> + <translation>Verbindung verloren. Nheko versucht sie wieder aufzunehmen…</translation> + </message> +</context> +<context> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation>-- verschlüsselter Event (keine Schlüssel zur Entschlüsselung gefunden) --</translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation>-- Entschlüsselungsfehler (Fehler bei Kommunikation mit Datenbank) --</translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation>-- Entschlüsselungsfehler (Fehler bei Suche nach megolm Schlüsseln in Datenbank) --</translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation>-- Entschlüsselungsfehler (%1) --</translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation>-- verschlüsselter Event (Unbekannter Eventtyp) --</translation> + </message> + <message> + <location line="+47"/> + <source>Message redaction failed: %1</source> + <translation>Nachricht zurückziehen fehlgeschlagen: %1</translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation>Bild speichern</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation>Video speichern</translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation>Audiodatei speichern</translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation>Datei speichern</translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation> + <numerusform>%1%2 tippt</numerusform> + <numerusform>%1 und %2 tippen</numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation>%1 wurde eingeladen.</translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation>%1 hat den Anzeigenamen und Avatar geändert.</translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation>%1 hat den Anzeigenamen geändert.</translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation>%1 hat den Avatar geändert.</translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation>%1 hat den Raum betreten.</translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation>%1 hat die Einladung abgewiesen.</translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation>Hat die Einladung an %1 zurückgezogen.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation>%1 hat den Raum verlassen.</translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation>%1 wurde gekickt.</translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation>Hat die Verbannung von %1 zurückgezogen.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation>%1 hat das Anklopfen zurückgezogen.</translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation>Hat das Anklopfen von %1 abgewiesen.</translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation>%1 hat den Raum verlassen.</translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation>%1 wurde gebannt.</translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation>%1 hat angeklopft.</translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation>Antworten</translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation>Optionen</translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">Lesebestätigungen</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished">Als gelesen markieren</translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished">Zeige rohen Nachrichteninhalt</translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished">Nachricht löschen</translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished">Speichern als...</translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation>Kein Raum geöffnet</translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished">Schließen</translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cc" line="+87"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> + <source>Room options</source> + <translation>Raumoptionen</translation> + </message> + <message> + <location line="+5"/> + <source>Mentions</source> + <translation>Erwähnungen</translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> <translation>Benutzer einladen</translation> </message> @@ -256,7 +696,7 @@ <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cc" line="+116"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> <translation>Zeigen</translation> </message> @@ -267,100 +707,209 @@ </message> </context> <context> - <name>TypingDisplay</name> + <name>UserInfoWidget</name> <message> - <location filename="../../src/TypingDisplay.cc" line="+26"/> - <source> is typing</source> - <translation> tippt</translation> - </message> - <message> - <location line="+2"/> - <source> are typing</source> - <translation> tippen</translation> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> + <source>Logout</source> + <translation>Abmelden</translation> </message> </context> <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cc" line="+121"/> - <source>User Settings</source> - <translation>Benutzereinstellungen</translation> - </message> - <message> - <location line="+15"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> <translation>Ins Benachrichtigungsfeld minimieren</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> <translation>Im Benachrichtigungsfeld starten</translation> </message> <message> - <location line="+12"/> - <source>Re-order rooms based on activity</source> - <translation>Räume nach Aktivität sortieren</translation> - </message> - <message> - <location line="+9"/> + <location line="+5"/> <source>Group's sidebar</source> <translation>Gruppen-Seitenleiste</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Circular Avatars</source> + <translation>Runde Profilbilder</translation> + </message> + <message> + <location line="+3"/> <source>Typing notifications</source> <translation>Schreibbenachrichtigungen</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> <translation>Lesebestätigungen</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Desktop notifications</source> + <translation>Desktopbenachrichtigungen</translation> + </message> + <message> + <location line="+4"/> + <source>Scale factor</source> + <translation>Skalierungsfaktor</translation> + </message> + <message> + <location line="+11"/> + <source>Font size</source> + <translation>Schriftgröße</translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation>Schriftart</translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation>Emoji Schriftart</translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> <translation>Erscheinungsbild</translation> </message> <message> - <location line="+10"/> + <location line="+27"/> + <source>Device ID</source> + <translation>Geräte-ID</translation> + </message> + <message> + <location line="+12"/> + <source>Device Fingerprint</source> + <translation>Gerätefingerabdruck</translation> + </message> + <message> + <location line="+11"/> + <source>Session Keys</source> + <translation>Sitzungsschlüssel</translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation>IMPORTIEREN</translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation>EXPORTIEREN</translation> + </message> + <message> + <location line="+13"/> + <source>ENCRYPTION</source> + <translation>VERSCHLÜSSELUNG</translation> + </message> + <message> + <location line="+4"/> <source>GENERAL</source> <translation>ALLGEMEINES</translation> </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation>Öffne Sessions Datei</translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation>Feher</translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation>Password für Datei</translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation>Bitte gib das Passwort zum Enschlüsseln der Datei ein:</translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation>Das Passwort darf nicht leer sein</translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation>Bitte gib das Passwort zum Verschlüsseln der Sitzungsschlüssel ein:</translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation>Datei zum Speichern der zu exportierenden Sitzungsschlüssel</translation> + </message> </context> <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cc" line="+44"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> - <translation>Willkommen bei nheko, dem Desktop-Client für das Matrix-Protokoll.</translation> + <translation>Willkommen bei nheko! Ein Desktop-Client für das Matrix-Protokoll.</translation> </message> <message> <location line="+1"/> <source>Enjoy your stay!</source> - <translation>Genieße deinen Aufenthalt!</translation> + <translation>Viel Vergnügen!</translation> </message> <message> - <location line="+19"/> + <location line="+23"/> <source>REGISTER</source> <translation>REGISTRIEREN</translation> </message> <message> - <location line="+6"/> + <location line="+5"/> <source>LOGIN</source> <translation>ANMELDEN</translation> </message> </context> <context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation>Gestern</translation> + </message> +</context> +<context> <name>dialogs::CreateRoom</name> <message> - <location filename="../../src/dialogs/CreateRoom.cc" line="+32"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation>Raum erstellen</translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation>Abbrechen</translation> + </message> + <message> + <location line="+10"/> <source>Name</source> - <translation>Titel</translation> + <translation>Raumname</translation> </message> <message> <location line="+3"/> @@ -370,7 +919,7 @@ <message> <location line="+3"/> <source>Alias</source> - <translation>Alias</translation> + <translation>Raumalias</translation> </message> <message> <location line="+8"/> @@ -378,12 +927,12 @@ <translation>Raumsichtbarkeit</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Room Preset</source> <translation>Raumvorlage</translation> </message> <message> - <location line="+10"/> + <location line="+9"/> <source>Direct Chat</source> <translation>Direkter Chat</translation> </message> @@ -391,12 +940,12 @@ <context> <name>dialogs::InviteUsers</name> <message> - <location filename="../../src/dialogs/InviteUsers.cc" line="+36"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation>Abbrechen</translation> </message> <message> - <location line="+11"/> + <location line="+8"/> <source>User ID to invite</source> <translation>Benutzer-ID, die eingeladen werden soll</translation> </message> @@ -404,12 +953,17 @@ <context> <name>dialogs::JoinRoom</name> <message> - <location filename="../../src/dialogs/JoinRoom.cc" line="+30"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation>Betreten</translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation>Abbrechen</translation> + </message> + <message> + <location line="+7"/> <source>Room ID or alias</source> <translation>Raum-ID oder -Alias</translation> </message> @@ -417,12 +971,12 @@ <context> <name>dialogs::LeaveRoom</name> <message> - <location filename="../../src/dialogs/LeaveRoom.cc" line="+29"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation>Abbrechen</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Are you sure you want to leave?</source> <translation>Willst du wirklich den Raum verlassen?</translation> </message> @@ -430,12 +984,12 @@ <context> <name>dialogs::Logout</name> <message> - <location filename="../../src/dialogs/Logout.cc" line="+47"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation>Abbrechen</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Logout. Are you sure?</source> <translation>Willst du dich wirklich abmelden?</translation> </message> @@ -443,7 +997,7 @@ <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cc" line="+41"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> <translation>Hochladen</translation> </message> @@ -453,7 +1007,7 @@ <translation>Abbrechen</translation> </message> <message> - <location line="+72"/> + <location line="+84"/> <source>Media type: %1 Media size: %2 </source> @@ -465,14 +1019,14 @@ Medien-Größe: %2 <context> <name>dialogs::ReCaptcha</name> <message> - <location filename="../../src/dialogs/ReCaptcha.cpp" line="+34"/> - <source>CONFIRM</source> - <translation>BESTÄTIGEN</translation> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation>Abbrechen</translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location line="+1"/> + <source>Confirm</source> + <translation>Bestätigen</translation> </message> <message> <location line="+11"/> @@ -483,30 +1037,63 @@ Medien-Größe: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cc" line="+98"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Lesebestätigungen</translation> </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation>Schließen</translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation>Heute %1</translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation>Gestern %1</translation> + </message> </context> <context> <name>dialogs::RoomSettings</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+147"/> - <source>CANCEL</source> - <translation>ABBRECHEN</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> + <source>Settings</source> + <translation>Einstellungen</translation> </message> <message> - <location line="+12"/> + <location line="+3"/> + <source>Info</source> + <translation>Informationen</translation> + </message> + <message> + <location line="+11"/> + <source>Internal ID</source> + <translation>interne ID</translation> + </message> + <message> + <location line="+10"/> + <source>Room Version</source> + <translation>Raumversion</translation> + </message> + <message> + <location line="+4"/> <source>Notifications</source> <translation>Benachrichtigungen</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> <translation>Stumm</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> <translation>Nur Erwähnungen</translation> </message> @@ -516,7 +1103,7 @@ Medien-Größe: %2 <translation>Alle Nachrichten</translation> </message> <message> - <location line="+8"/> + <location line="+97"/> <source>Room access</source> <translation>Raumzugang</translation> </message> @@ -535,11 +1122,105 @@ Medien-Größe: %2 <source>Invited users</source> <translation>Nur Eingeladene</translation> </message> + <message> + <location line="+50"/> + <source>Encryption</source> + <translation>Verschlüsselung</translation> + </message> + <message> + <location line="+8"/> + <source>End-to-End Encryption</source> + <translation>Ende-zu-Ende Verschlüsselung</translation> + </message> + <message> + <location line="+1"/> + <source>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</source> + <translation>Verschlüsselung befindet sich momentan in einem experimentellen Stadium, unerwartete Fehler können auftreten. <br>Sie kann anschließend nicht wieder deaktiviert werden.</translation> + </message> + <message> + <location line="+27"/> + <source>Respond to key requests</source> + <translation>Schlüsselnfrage beantworten</translation> + </message> + <message> + <location line="+3"/> + <source>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</source> + <translation>Ob nheko automatisch auf Anfragen mit Sessionschlüsseln antworten soll, oder nicht. Bitte mit Vorsicht nutzen, da dies eine temporäre Massnahme ist. Sie dient dem Test von E2E Verschlüsselung, bis die Geräteverifikation fertig gestellt ist.</translation> + </message> + <message numerus="yes"> + <location line="+51"/> + <source>%n member(s)</source> + <translation> + <numerusform>%n Teilnehmer</numerusform> + <numerusform>%n Teilnehmer</numerusform> + </translation> + </message> + <message> + <location line="+140"/> + <source>Failed to enable encryption: %1</source> + <translation>Aktivierung der Verschlüsselung fehlgeschlagen: %1</translation> + </message> + <message> + <location line="+145"/> + <source>Select an avatar</source> + <translation>Wähle einen Avatar</translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation>Alle Dateien (*)</translation> + </message> + <message> + <location line="+12"/> + <source>The selected file is not an image</source> + <translation>Die ausgewählte Datei ist kein Bild</translation> + </message> + <message> + <location line="+5"/> + <source>Error while reading file: %1</source> + <translation>Fehler beim Lesen der DateI: %1</translation> + </message> + <message> + <location line="+35"/> + <location line="+20"/> + <source>Failed to upload image: %s</source> + <translation>Hochladen der Bilddatei fehlgeschlagen: %s</translation> + </message> +</context> +<context> + <name>dialogs::UserProfile</name> + <message> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> + <source>Ban the user from the room</source> + <translation>Banne den Nutzer aus diesem Raum</translation> + </message> + <message> + <location line="+8"/> + <source>Ignore messages from this user</source> + <translation>Nachrichten von diesem Nutzer ignorieren</translation> + </message> + <message> + <location line="+9"/> + <source>Kick the user from the room</source> + <translation>Entferne diesen Nutzer aus dem Raum</translation> + </message> + <message> + <location line="+8"/> + <source>Start a conversation</source> + <translation>Gespräch beginnen</translation> + </message> + <message> + <location line="+63"/> + <source>Devices</source> + <translation>Geräte</translation> + </message> </context> <context> <name>emoji::Panel</name> <message> - <location filename="../../src/emoji/Panel.cc" line="+125"/> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> <source>Smileys & People</source> <translation>Smileys & Personen</translation> </message> @@ -579,4 +1260,108 @@ Medien-Größe: %2 <translation>Flaggen</translation> </message> </context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation>Du hast eine Audiodatei gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation>%1 hat eine Audiodatei gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation>Du hast ein Bild gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation>%1 hat ein Bild gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation>Du hast eine Datei gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation>%1 hat eine Datei gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation>Du hast ein Video gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation>%1 hat ein Video gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation>Du hast einen Sticker gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation>%1 hat einen Sticker gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation>Du hast eine Benachrichtigung gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation>%1 hat eine Benachrichtigung gesendet.</translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation>Du: %1</translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation>%1: %2</translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation>Du hast eine verschlüsselte Nachricht gesendet.</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation>%1 hat eine verschlüsselte Nachricht gesendet.</translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation>Dieser Raum</translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation>Alle Räume</translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation>Unbekannter Nachrichtentyp</translation> + </message> +</context> </TS> diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index 1f993fea..39471c09 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -2,40 +2,153 @@ <!DOCTYPE TS> <TS version="2.1" language="el"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cc" line="+125"/> - <source>Save File</source> - <translation>Αποθήκευση</translation> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+399"/> + <source>Failed to restore OLM account. Please login again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Failed to restore save data. Please login again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> + <source>Please try to login again: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Room creation failed: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Failed to leave room: %1</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>DateSeparator</name> + <name>CommunitiesListItem</name> + <message> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/timeline/TimelineView.cc" line="+54"/> - <source>Today</source> - <translation>Σήμερα</translation> + <location line="+4"/> + <source>Favourite rooms</source> + <translation type="unfinished"></translation> </message> <message> <location line="+2"/> - <source>Yesterday</source> - <translation>Χθές</translation> + <source>Low priority rooms</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>EditModal</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+34"/> - <source>APPLY</source> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> <translation type="unfinished"></translation> </message> <message> - <location line="+2"/> - <source>CANCEL</source> - <translation type="unfinished">ΑΚΥΡΟ</translation> + <location line="+1"/> + <source>Cancel</source> + <translation type="unfinished">Άκυρο</translation> </message> <message> - <location line="+9"/> + <location line="+10"/> <source>Name</source> <translation type="unfinished">Όνομα</translation> </message> @@ -46,25 +159,25 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cc" line="+111"/> - <source>Save File</source> - <translation>Αποθήκευση</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>ImageItem</name> + <name>InviteeItem</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cc" line="+229"/> - <source>Save image</source> - <translation>Αποθήκευση Εικόνας</translation> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cc" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> <translation>Matrix ID</translation> </message> @@ -79,56 +192,104 @@ <translation>Κωδικός</translation> </message> <message> + <location line="+4"/> + <source>Device name</source> + <translation type="unfinished"></translation> + </message> + <message> <location line="+19"/> <source>LOGIN</source> <translation>ΕΙΣΟΔΟΣ</translation> </message> <message> - <location line="+128"/> - <source>Empty password</source> - <translation>Κενός κωδικός</translation> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation type="unfinished"></translation> </message> -</context> -<context> - <name>MatrixClient</name> <message> - <location filename="../../src/MatrixClient.cc" line="+164"/> - <source>Wrong username or password</source> - <translation>Λανθασμένο όνμα χρήστη ή κωδικός</translation> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+5"/> - <source>Login endpoint was not found on the server</source> + <location line="+24"/> + <source>The required endpoints were not found. Possibly not a Matrix server.</source> <translation type="unfinished"></translation> </message> <message> <location line="+6"/> - <source>An unknown error occured. Please try again.</source> + <source>Received malformed response. Make sure the homeserver domain is valid.</source> <translation type="unfinished"></translation> </message> <message> - <location line="+23"/> - <source>Malformed response. Possibly not a Matrix server</source> + <location line="+5"/> + <source>An unknown error occured. Make sure the homeserver domain is valid.</source> <translation type="unfinished"></translation> </message> + <message> + <location line="+60"/> + <source>Empty password</source> + <translation>Κενός κωδικός</translation> + </message> </context> <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+79"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> <source>Room members</source> <translation>Μέλη</translation> </message> <message> - <location line="+10"/> - <source>SHOW MORE</source> - <translation>ΠΕΡΙΣΣΟΤΕΡΑ</translation> + <location line="+4"/> + <source>OK</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cc" line="+70"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> <translation>Αναζήτηση συνομιλίας...</translation> </message> @@ -136,7 +297,7 @@ <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cc" line="+76"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> <translation>Όνομα χρήστη</translation> </message> @@ -156,12 +317,12 @@ <translation>Διακομιστής</translation> </message> <message> - <location line="+17"/> + <location line="+16"/> <source>REGISTER</source> <translation>ΕΓΓΡΑΦΗ</translation> </message> <message> - <location line="+76"/> + <location line="+93"/> <source>Invalid username</source> <translation>Μη έγκυρο όνομα χρήστη</translation> </message> @@ -182,19 +343,35 @@ </message> </context> <context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cc" line="+78"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Βγές</translation> </message> <message> - <location line="+153"/> + <location line="+151"/> <source>Accept</source> <translation>Αποδοχή</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> <translation>Απόρριψη</translation> </message> @@ -202,7 +379,12 @@ <context> <name>SideBarActions</name> <message> - <location filename="../../src/SideBarActions.cc" line="+36"/> + <location filename="../../src/SideBarActions.cpp" line="+38"/> + <source>User settings</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> <source>Create new room</source> <translation>Νέα συνομιλία</translation> </message> @@ -211,16 +393,65 @@ <source>Join a room</source> <translation type="unfinished"></translation> </message> + <message> + <location line="+16"/> + <source>Start a new chat</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>Room directory</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>StatusIndicator</name> + <message> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cc" line="+445"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> + <source>Send a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> <translation>Γράψε ένα μήνυμα...</translation> </message> <message> - <location line="+108"/> + <location line="+31"/> + <source>Send a message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Emoji</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+85"/> <source>Select a file</source> <translation>Διάλεξε ένα αρχείο</translation> </message> @@ -229,11 +460,220 @@ <source>All Files (*)</source> <translation>Όλα τα αρχεία (*)</translation> </message> + <message> + <location filename="../../src/TextInputWidget.h" line="-5"/> + <source>Connection lost. Nheko is trying to re-connect...</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+47"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">Αποθήκευση Εικόνας</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cc" line="+87"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> + <source>Room options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Mentions</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> <translation>Προσκάλεσε χρήστες</translation> </message> @@ -256,7 +696,7 @@ <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cc" line="+116"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> <translation>Εμφάνιση</translation> </message> @@ -267,70 +707,166 @@ </message> </context> <context> - <name>TypingDisplay</name> - <message> - <location filename="../../src/TypingDisplay.cc" line="+26"/> - <source> is typing</source> - <translation> πληκτρολογεί</translation> - </message> + <name>UserInfoWidget</name> <message> - <location line="+2"/> - <source> are typing</source> - <translation> πληκτρολογούν</translation> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> + <source>Logout</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cc" line="+121"/> - <source>User Settings</source> - <translation>Ρυθμίσεις Χρήστη</translation> - </message> - <message> - <location line="+15"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> <translation>Ελαχιστοποίηση</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> <translation type="unfinished"></translation> </message> <message> - <location line="+12"/> - <source>Re-order rooms based on activity</source> + <location line="+5"/> + <source>Group's sidebar</source> <translation type="unfinished"></translation> </message> <message> - <location line="+9"/> - <source>Group's sidebar</source> + <location line="+3"/> + <source>Circular Avatars</source> <translation type="unfinished"></translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Typing notifications</source> <translation type="unfinished"></translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> <translation type="unfinished"></translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Desktop notifications</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Scale factor</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Font size</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> <translation>Φόντο</translation> </message> <message> - <location line="+10"/> + <location line="+27"/> + <source>Device ID</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Device Fingerprint</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Session Keys</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>ENCRYPTION</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> <source>GENERAL</source> <translation>ΓΕΝΙΚΑ</translation> </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cc" line="+44"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> <translation>Καλως ήρθες στο nheko!</translation> </message> @@ -340,25 +876,38 @@ <translation> </translation> </message> <message> - <location line="+19"/> + <location line="+23"/> <source>REGISTER</source> <translation>ΕΓΓΡΑΦΗ</translation> </message> <message> - <location line="+6"/> + <location line="+5"/> <source>LOGIN</source> <translation>ΕΙΣΟΔΟΣ</translation> </message> </context> <context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>dialogs::CreateRoom</name> <message> - <location filename="../../src/dialogs/CreateRoom.cc" line="+32"/> - <source>CANCEL</source> - <translation>ΑΚΥΡΟ</translation> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Άκυρο</translation> + </message> + <message> + <location line="+10"/> <source>Name</source> <translation>Όνομα</translation> </message> @@ -378,12 +927,12 @@ <translation type="unfinished"></translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Room Preset</source> <translation type="unfinished"></translation> </message> <message> - <location line="+10"/> + <location line="+9"/> <source>Direct Chat</source> <translation>Άμεση συνομιλία</translation> </message> @@ -391,12 +940,12 @@ <context> <name>dialogs::InviteUsers</name> <message> - <location filename="../../src/dialogs/InviteUsers.cc" line="+36"/> - <source>CANCEL</source> - <translation>ΑΚΥΡΟ</translation> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation type="unfinished">Άκυρο</translation> </message> <message> - <location line="+11"/> + <location line="+8"/> <source>User ID to invite</source> <translation>Όνομα χρήστη</translation> </message> @@ -404,12 +953,17 @@ <context> <name>dialogs::JoinRoom</name> <message> - <location filename="../../src/dialogs/JoinRoom.cc" line="+30"/> - <source>CANCEL</source> - <translation>ΑΚΥΡΟ</translation> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Άκυρο</translation> </message> <message> - <location line="+11"/> + <location line="+7"/> <source>Room ID or alias</source> <translation>ID ή όνομα συνομιλίας</translation> </message> @@ -417,12 +971,12 @@ <context> <name>dialogs::LeaveRoom</name> <message> - <location filename="../../src/dialogs/LeaveRoom.cc" line="+29"/> - <source>CANCEL</source> - <translation>ΑΚΥΡΟ</translation> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Άκυρο</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Are you sure you want to leave?</source> <translation>Είστε σίγουροι οτι θέλετε να κλείσετε τη συνομιλία;</translation> </message> @@ -430,12 +984,12 @@ <context> <name>dialogs::Logout</name> <message> - <location filename="../../src/dialogs/Logout.cc" line="+47"/> - <source>CANCEL</source> - <translation>ΑΚΥΡΟ</translation> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation type="unfinished">Άκυρο</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Logout. Are you sure?</source> <translation>Αποσύνδεση. Είστε σίγουροι;</translation> </message> @@ -443,7 +997,7 @@ <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cc" line="+41"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> <translation>Μεταφόρτωση</translation> </message> @@ -453,7 +1007,7 @@ <translation>Άκυρο</translation> </message> <message> - <location line="+72"/> + <location line="+84"/> <source>Media type: %1 Media size: %2 </source> @@ -463,14 +1017,14 @@ Media size: %2 <context> <name>dialogs::ReCaptcha</name> <message> - <location filename="../../src/dialogs/ReCaptcha.cpp" line="+34"/> - <source>CONFIRM</source> - <translation>ΕΠΙΒΕΒΑΙΩΣΗ</translation> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Άκυρο</translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation>ΑΚΥΡΟ</translation> + <location line="+1"/> + <source>Confirm</source> + <translation type="unfinished"></translation> </message> <message> <location line="+11"/> @@ -481,30 +1035,63 @@ Media size: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cc" line="+98"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation type="unfinished"></translation> </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>dialogs::RoomSettings</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+147"/> - <source>CANCEL</source> - <translation>ΑΚΥΡΟ</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> + <source>Settings</source> + <translation type="unfinished">Ρυθμίσεις</translation> </message> <message> - <location line="+12"/> + <location line="+3"/> + <source>Info</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Internal ID</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+10"/> + <source>Room Version</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> <source>Notifications</source> <translation>Ειδοποιήσεις</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> <translation>Αθόρυβο</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> <translation>Αναφορές μόνο</translation> </message> @@ -514,7 +1101,7 @@ Media size: %2 <translation type="unfinished"></translation> </message> <message> - <location line="+8"/> + <location line="+97"/> <source>Room access</source> <translation type="unfinished"></translation> </message> @@ -533,11 +1120,105 @@ Media size: %2 <source>Invited users</source> <translation>Μόνο με πρόσκληση</translation> </message> + <message> + <location line="+50"/> + <source>Encryption</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>End-to-End Encryption</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+27"/> + <source>Respond to key requests</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+51"/> + <source>%n member(s)</source> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+140"/> + <source>Failed to enable encryption: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+145"/> + <source>Select an avatar</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation type="unfinished">Όλα τα αρχεία (*)</translation> + </message> + <message> + <location line="+12"/> + <source>The selected file is not an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Error while reading file: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+35"/> + <location line="+20"/> + <source>Failed to upload image: %s</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::UserProfile</name> + <message> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> + <source>Ban the user from the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Ignore messages from this user</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>Kick the user from the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Start a conversation</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+63"/> + <source>Devices</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>emoji::Panel</name> <message> - <location filename="../../src/emoji/Panel.cc" line="+125"/> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> <source>Smileys & People</source> <translation>Πρόσωπα</translation> </message> @@ -577,4 +1258,108 @@ Media size: %2 <translation>Σημαίες</translation> </message> </context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation type="unfinished"></translation> + </message> +</context> </TS> diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index 4f4db00d..ba462b41 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -2,579 +2,1368 @@ <!DOCTYPE TS> <TS version="2.1" language="en"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cc" line="+125"/> - <source>Save File</source> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> <translation type="unfinished"></translation> </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+399"/> + <source>Failed to restore OLM account. Please login again.</source> + <translation>Failed to restore OLM account. Please login again.</translation> + </message> + <message> + <location line="+5"/> + <source>Failed to restore save data. Please login again.</source> + <translation>Failed to restore save data. Please login again.</translation> + </message> + <message> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> + <source>Please try to login again: %1</source> + <translation>Please try to login again: %1</translation> + </message> + <message> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Room creation failed: %1</source> + <translation>Room creation failed: %1</translation> + </message> + <message> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Failed to leave room: %1</source> + <translation>Failed to leave room: %1</translation> + </message> </context> <context> - <name>DateSeparator</name> + <name>CommunitiesListItem</name> <message> - <location filename="../../src/timeline/TimelineView.cc" line="+54"/> - <source>Today</source> - <translation type="unfinished"></translation> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation>All rooms</translation> + </message> + <message> + <location line="+4"/> + <source>Favourite rooms</source> + <translation>Favourite rooms</translation> </message> <message> <location line="+2"/> - <source>Yesterday</source> - <translation type="unfinished"></translation> + <source>Low priority rooms</source> + <translation>Low priority rooms</translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation> (tag)</translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation> (community)</translation> </message> </context> <context> <name>EditModal</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+34"/> - <source>APPLY</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> + <translation>Apply</translation> </message> <message> - <location line="+2"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location line="+1"/> + <source>Cancel</source> + <translation>Cancel</translation> </message> <message> - <location line="+9"/> + <location line="+10"/> <source>Name</source> - <translation type="unfinished"></translation> + <translation>Name</translation> </message> <message> <location line="+2"/> <source>Topic</source> - <translation type="unfinished"></translation> + <translation>Topic</translation> </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cc" line="+111"/> - <source>Save File</source> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>ImageItem</name> + <name>InviteeItem</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cc" line="+229"/> - <source>Save image</source> - <translation type="unfinished"></translation> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation>Remove</translation> </message> </context> <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cc" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> - <translation type="unfinished"></translation> + <translation>Matrix ID</translation> </message> <message> <location line="+1"/> <source>e.g @joe:matrix.org</source> - <translation type="unfinished"></translation> + <translation>e.g @joe:matrix.org</translation> </message> <message> <location line="+15"/> <source>Password</source> - <translation type="unfinished"></translation> + <translation>Password</translation> + </message> + <message> + <location line="+4"/> + <source>Device name</source> + <translation>Device name</translation> </message> <message> <location line="+19"/> <source>LOGIN</source> - <translation type="unfinished"></translation> + <translation>LOGIN</translation> + </message> + <message> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation>Autodiscovery failed. Received malformed response.</translation> + </message> + <message> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation>Autodiscovery failed. Unknown error while requesting .well-known.</translation> + </message> + <message> + <location line="+24"/> + <source>The required endpoints were not found. Possibly not a Matrix server.</source> + <translation>The required endpoints were not found. Possibly not a Matrix server.</translation> + </message> + <message> + <location line="+6"/> + <source>Received malformed response. Make sure the homeserver domain is valid.</source> + <translation>Received malformed response. Make sure the homeserver domain is valid.</translation> + </message> + <message> + <location line="+5"/> + <source>An unknown error occured. Make sure the homeserver domain is valid.</source> + <translation>An unknown error occured. Make sure the homeserver domain is valid.</translation> </message> <message> - <location line="+128"/> + <location line="+60"/> <source>Empty password</source> - <translation type="unfinished"></translation> + <translation>Empty password</translation> + </message> +</context> +<context> + <name>MemberList</name> + <message> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> + <source>Room members</source> + <translation>Room members</translation> + </message> + <message> + <location line="+4"/> + <source>OK</source> + <translation>OK</translation> </message> </context> <context> - <name>MatrixClient</name> + <name>MessageDelegate</name> <message> - <location filename="../../src/MatrixClient.cc" line="+164"/> - <source>Wrong username or password</source> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> <translation type="unfinished"></translation> </message> <message> - <location line="+5"/> - <source>Login endpoint was not found on the server</source> + <location line="+6"/> + <source>Encryption enabled</source> <translation type="unfinished"></translation> </message> <message> <location line="+6"/> - <source>An unknown error occured. Please try again.</source> + <source>room name changed to: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="+23"/> - <source>Malformed response. Possibly not a Matrix server</source> + <location line="+0"/> + <source>removed room name</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+79"/> - <source>Room members</source> + <location line="+6"/> + <source>topic changed to: %1</source> <translation type="unfinished"></translation> </message> <message> - <location line="+10"/> - <source>SHOW MORE</source> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> <translation type="unfinished"></translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cc" line="+70"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> - <translation type="unfinished"></translation> + <translation>Search for a room…</translation> </message> </context> <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cc" line="+76"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> - <translation type="unfinished"></translation> + <translation>Username</translation> </message> <message> <location line="+3"/> <source>Password</source> - <translation type="unfinished"></translation> + <translation>Password</translation> </message> <message> <location line="+4"/> <source>Password confirmation</source> - <translation type="unfinished"></translation> + <translation>Password confirmation</translation> </message> <message> <location line="+4"/> <source>Home Server</source> - <translation type="unfinished"></translation> + <translation>Home Server</translation> </message> <message> - <location line="+17"/> + <location line="+16"/> <source>REGISTER</source> - <translation type="unfinished"></translation> + <translation>REGISTER</translation> </message> <message> - <location line="+76"/> + <location line="+93"/> <source>Invalid username</source> - <translation type="unfinished"></translation> + <translation>Invalid username</translation> </message> <message> <location line="+2"/> <source>Password is not long enough (min 8 chars)</source> - <translation type="unfinished"></translation> + <translation>Password is not long enough (min 8 chars)</translation> </message> <message> <location line="+2"/> <source>Passwords don't match</source> - <translation type="unfinished"></translation> + <translation>Passwords don't match</translation> </message> <message> <location line="+2"/> <source>Invalid server name</source> - <translation type="unfinished"></translation> + <translation>Invalid server name</translation> + </message> +</context> +<context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation>Logout</translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation>no version stored</translation> </message> </context> <context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cc" line="+78"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> - <translation type="unfinished"></translation> + <translation>Leave room</translation> </message> <message> - <location line="+153"/> + <location line="+151"/> <source>Accept</source> - <translation type="unfinished"></translation> + <translation>Accept</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> - <translation type="unfinished"></translation> + <translation>Decline</translation> </message> </context> <context> <name>SideBarActions</name> <message> - <location filename="../../src/SideBarActions.cc" line="+36"/> + <location filename="../../src/SideBarActions.cpp" line="+38"/> + <source>User settings</source> + <translation>User settings</translation> + </message> + <message> + <location line="+7"/> <source>Create new room</source> - <translation type="unfinished"></translation> + <translation>Create new room</translation> </message> <message> <location line="+1"/> <source>Join a room</source> + <translation>Join a room</translation> + </message> + <message> + <location line="+16"/> + <source>Start a new chat</source> + <translation>Start a new chat</translation> + </message> + <message> + <location line="+15"/> + <source>Room directory</source> + <translation>Room directory</translation> + </message> +</context> +<context> + <name>StatusIndicator</name> + <message> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Read</source> <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cc" line="+445"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> + <source>Send a file</source> + <translation>Send a file</translation> + </message> + <message> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> - <translation type="unfinished"></translation> + <translation>Write a message…</translation> + </message> + <message> + <location line="+31"/> + <source>Send a message</source> + <translation>Send a message</translation> </message> <message> - <location line="+108"/> + <location line="+8"/> + <source>Emoji</source> + <translation>Emoji</translation> + </message> + <message> + <location line="+85"/> <source>Select a file</source> - <translation type="unfinished"></translation> + <translation>Select a file</translation> </message> <message> <location line="+0"/> <source>All Files (*)</source> + <translation>All Files (*)</translation> + </message> + <message> + <location filename="../../src/TextInputWidget.h" line="-5"/> + <source>Connection lost. Nheko is trying to re-connect...</source> + <translation>Connection lost. Nheko is trying to re-connect…</translation> + </message> +</context> +<context> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished">-- Encrypted Event (No keys found for decryption) --</translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished">-- Decryption Error (failed to communicate with DB) --</translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished">-- Decryption Error (failed to retrieve megolm keys from db) --</translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished">-- Decryption Error (%1) --</translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished">-- Encrypted Event (Unknown event type) --</translation> + </message> + <message> + <location line="+47"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished">Message redaction failed: %1</translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">Save image</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform>%1%2 is typing</numerusform> + <numerusform>%1 and %2 are typing</numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> <translation type="unfinished"></translation> </message> </context> <context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">Read receipts</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished">Close</translation> + </message> +</context> +<context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cc" line="+87"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> + <source>Room options</source> + <translation>Room options</translation> + </message> + <message> + <location line="+5"/> + <source>Mentions</source> + <translation>Mentions</translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> - <translation type="unfinished"></translation> + <translation>Invite users</translation> </message> <message> <location line="+6"/> <source>Members</source> - <translation type="unfinished"></translation> + <translation>Members</translation> </message> <message> <location line="+5"/> <source>Leave room</source> - <translation type="unfinished"></translation> + <translation>Leave room</translation> </message> <message> <location line="+5"/> <source>Settings</source> - <translation type="unfinished"></translation> + <translation>Settings</translation> </message> </context> <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cc" line="+116"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> - <translation type="unfinished"></translation> + <translation>Show</translation> </message> <message> <location line="+1"/> <source>Quit</source> - <translation type="unfinished"></translation> + <translation>Quit</translation> </message> </context> <context> - <name>TypingDisplay</name> + <name>UserInfoWidget</name> <message> - <location filename="../../src/TypingDisplay.cc" line="+26"/> - <source> is typing</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+2"/> - <source> are typing</source> - <translation type="unfinished"></translation> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> + <source>Logout</source> + <translation>Logout</translation> </message> </context> <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cc" line="+121"/> - <source>User Settings</source> - <translation type="unfinished"></translation> - </message> - <message> - <location line="+15"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> - <translation type="unfinished"></translation> + <translation>Minimize to tray</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> - <translation type="unfinished"></translation> + <translation>Start in tray</translation> </message> <message> - <location line="+12"/> - <source>Re-order rooms based on activity</source> - <translation type="unfinished"></translation> + <location line="+5"/> + <source>Group's sidebar</source> + <translation>Group's sidebar</translation> </message> <message> - <location line="+9"/> - <source>Group's sidebar</source> + <location line="+3"/> + <source>Circular Avatars</source> <translation type="unfinished"></translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Typing notifications</source> - <translation type="unfinished"></translation> + <translation>Typing notifications</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> + <translation>Read receipts</translation> + </message> + <message> + <location line="+3"/> + <source>Send messages as Markdown</source> <translation type="unfinished"></translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Desktop notifications</source> + <translation>Desktop notifications</translation> + </message> + <message> + <location line="+4"/> + <source>Scale factor</source> + <translation>Scale factor</translation> + </message> + <message> + <location line="+11"/> + <source>Font size</source> + <translation>Font size</translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation>Font Family</translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation>Emoji Font Family</translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> - <translation type="unfinished"></translation> + <translation>Theme</translation> </message> <message> - <location line="+10"/> + <location line="+27"/> + <source>Device ID</source> + <translation>Device ID</translation> + </message> + <message> + <location line="+12"/> + <source>Device Fingerprint</source> + <translation>Device Fingerprint</translation> + </message> + <message> + <location line="+11"/> + <source>Session Keys</source> + <translation>Session Keys</translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation>IMPORT</translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation>EXPORT</translation> + </message> + <message> + <location line="+13"/> + <source>ENCRYPTION</source> + <translation>ENCRYPTION</translation> + </message> + <message> + <location line="+4"/> <source>GENERAL</source> - <translation type="unfinished"></translation> + <translation>GENERAL</translation> + </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation>Open Sessions File</translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation>Error</translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation>File Password</translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation>Enter the passphrase to decrypt the file:</translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation>The password cannot be empty</translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation>Enter passphrase to encrypt your session keys:</translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation>File to save the exported session keys</translation> </message> </context> <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cc" line="+44"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> - <translation type="unfinished"></translation> + <translation>Welcome to nheko! The desktop client for the Matrix protocol.</translation> </message> <message> <location line="+1"/> <source>Enjoy your stay!</source> - <translation type="unfinished"></translation> + <translation>Enjoy your stay!</translation> </message> <message> - <location line="+19"/> + <location line="+23"/> <source>REGISTER</source> - <translation type="unfinished"></translation> + <translation>REGISTER</translation> </message> <message> - <location line="+6"/> + <location line="+5"/> <source>LOGIN</source> - <translation type="unfinished"></translation> + <translation>LOGIN</translation> + </message> +</context> +<context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation>Yesterday</translation> </message> </context> <context> <name>dialogs::CreateRoom</name> <message> - <location filename="../../src/dialogs/CreateRoom.cc" line="+32"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation>Create room</translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation>Cancel</translation> + </message> + <message> + <location line="+10"/> <source>Name</source> - <translation type="unfinished"></translation> + <translation>Name</translation> </message> <message> <location line="+3"/> <source>Topic</source> - <translation type="unfinished"></translation> + <translation>Topic</translation> </message> <message> <location line="+3"/> <source>Alias</source> - <translation type="unfinished"></translation> + <translation>Alias</translation> </message> <message> <location line="+8"/> <source>Room Visibility</source> - <translation type="unfinished"></translation> + <translation>Room Visibility</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Room Preset</source> - <translation type="unfinished"></translation> + <translation>Room Preset</translation> </message> <message> - <location line="+10"/> + <location line="+9"/> <source>Direct Chat</source> - <translation type="unfinished"></translation> + <translation>Direct Chat</translation> </message> </context> <context> <name>dialogs::InviteUsers</name> <message> - <location filename="../../src/dialogs/InviteUsers.cc" line="+36"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation>Cancel</translation> </message> <message> - <location line="+11"/> + <location line="+8"/> <source>User ID to invite</source> - <translation type="unfinished"></translation> + <translation>User ID to invite</translation> </message> </context> <context> <name>dialogs::JoinRoom</name> <message> - <location filename="../../src/dialogs/JoinRoom.cc" line="+30"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation>Join</translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation>Cancel</translation> + </message> + <message> + <location line="+7"/> <source>Room ID or alias</source> - <translation type="unfinished"></translation> + <translation>Room ID or alias</translation> </message> </context> <context> <name>dialogs::LeaveRoom</name> <message> - <location filename="../../src/dialogs/LeaveRoom.cc" line="+29"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation>Cancel</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Are you sure you want to leave?</source> - <translation type="unfinished"></translation> + <translation>Are you sure you want to leave?</translation> </message> </context> <context> <name>dialogs::Logout</name> <message> - <location filename="../../src/dialogs/Logout.cc" line="+47"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation>Cancel</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Logout. Are you sure?</source> - <translation type="unfinished"></translation> + <translation>Logout. Are you sure?</translation> </message> </context> <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cc" line="+41"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> - <translation type="unfinished"></translation> + <translation>Upload</translation> </message> <message> <location line="+1"/> <source>Cancel</source> - <translation type="unfinished"></translation> + <translation>Cancel</translation> </message> <message> - <location line="+72"/> + <location line="+84"/> <source>Media type: %1 Media size: %2 </source> - <translation type="unfinished"></translation> + <translation>Media type: %1 +Media size: %2 +</translation> </message> </context> <context> <name>dialogs::ReCaptcha</name> <message> - <location filename="../../src/dialogs/ReCaptcha.cpp" line="+34"/> - <source>CONFIRM</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation>Cancel</translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location line="+1"/> + <source>Confirm</source> + <translation>Confirm</translation> </message> <message> <location line="+11"/> <source>Solve the reCAPTCHA and press the confirm button</source> - <translation type="unfinished"></translation> + <translation>Solve the reCAPTCHA and press the confirm button</translation> </message> </context> <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cc" line="+98"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> - <translation type="unfinished"></translation> + <translation>Read receipts</translation> + </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation>Close</translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation>Today %1</translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation>Yesterday %1</translation> </message> </context> <context> <name>dialogs::RoomSettings</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+147"/> - <source>CANCEL</source> - <translation type="unfinished"></translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> + <source>Settings</source> + <translation>Settings</translation> </message> <message> - <location line="+12"/> + <location line="+3"/> + <source>Info</source> + <translation>Info</translation> + </message> + <message> + <location line="+11"/> + <source>Internal ID</source> + <translation>Internal ID</translation> + </message> + <message> + <location line="+10"/> + <source>Room Version</source> + <translation>Room Version</translation> + </message> + <message> + <location line="+4"/> <source>Notifications</source> - <translation type="unfinished"></translation> + <translation>Notifications</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> - <translation type="unfinished"></translation> + <translation>Muted</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> - <translation type="unfinished"></translation> + <translation>Mentions only</translation> </message> <message> <location line="+1"/> <source>All messages</source> - <translation type="unfinished"></translation> + <translation>All messages</translation> </message> <message> - <location line="+8"/> + <location line="+97"/> <source>Room access</source> - <translation type="unfinished"></translation> + <translation>Room access</translation> </message> <message> <location line="+2"/> <source>Anyone and guests</source> - <translation type="unfinished"></translation> + <translation>Anyone and guests</translation> </message> <message> <location line="+1"/> <source>Anyone</source> - <translation type="unfinished"></translation> + <translation>Anyone who knows the room link (no guests)</translation> </message> <message> <location line="+1"/> <source>Invited users</source> - <translation type="unfinished"></translation> + <translation>Invited users</translation> + </message> + <message> + <location line="+50"/> + <source>Encryption</source> + <translation>Encryption</translation> + </message> + <message> + <location line="+8"/> + <source>End-to-End Encryption</source> + <translation>End-to-End Encryption</translation> + </message> + <message> + <location line="+1"/> + <source>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</source> + <translation>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</translation> + </message> + <message> + <location line="+27"/> + <source>Respond to key requests</source> + <translation>Respond to key requests</translation> + </message> + <message> + <location line="+3"/> + <source>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</source> + <translation>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</translation> + </message> + <message numerus="yes"> + <location line="+51"/> + <source>%n member(s)</source> + <translation> + <numerusform>%n member</numerusform> + <numerusform>%n members</numerusform> + </translation> + </message> + <message> + <location line="+140"/> + <source>Failed to enable encryption: %1</source> + <translation>Failed to enable encryption: %1</translation> + </message> + <message> + <location line="+145"/> + <source>Select an avatar</source> + <translation>Select an avatar</translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation>All Files (*)</translation> + </message> + <message> + <location line="+12"/> + <source>The selected file is not an image</source> + <translation>The selected file is not an image</translation> + </message> + <message> + <location line="+5"/> + <source>Error while reading file: %1</source> + <translation>Error while reading file: %1</translation> + </message> + <message> + <location line="+35"/> + <location line="+20"/> + <source>Failed to upload image: %s</source> + <translation>Failed to upload image: %s</translation> + </message> +</context> +<context> + <name>dialogs::UserProfile</name> + <message> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> + <source>Ban the user from the room</source> + <translation>Ban the user from the room</translation> + </message> + <message> + <location line="+8"/> + <source>Ignore messages from this user</source> + <translation>Ignore messages from this user</translation> + </message> + <message> + <location line="+9"/> + <source>Kick the user from the room</source> + <translation>Kick the user from the room</translation> + </message> + <message> + <location line="+8"/> + <source>Start a conversation</source> + <translation>Start a conversation</translation> + </message> + <message> + <location line="+63"/> + <source>Devices</source> + <translation>Devices</translation> </message> </context> <context> <name>emoji::Panel</name> <message> - <location filename="../../src/emoji/Panel.cc" line="+125"/> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> <source>Smileys & People</source> - <translation type="unfinished">Smileys & People</translation> + <translation>Smileys & People</translation> </message> <message> <location line="+4"/> <source>Animals & Nature</source> - <translation type="unfinished">Animals & Nature</translation> + <translation>Animals & Nature</translation> </message> <message> <location line="+3"/> <source>Food & Drink</source> - <translation type="unfinished">Food & Drink</translation> + <translation>Food & Drink</translation> </message> <message> <location line="+3"/> <source>Activity</source> - <translation type="unfinished">Activity</translation> + <translation>Activity</translation> </message> <message> <location line="+4"/> <source>Travel & Places</source> - <translation type="unfinished">Travel & Places</translation> + <translation>Travel & Places</translation> </message> <message> <location line="+3"/> <source>Objects</source> - <translation type="unfinished">Objects</translation> + <translation>Objects</translation> </message> <message> <location line="+3"/> <source>Symbols</source> - <translation type="unfinished">Symbols</translation> + <translation>Symbols</translation> </message> <message> <location line="+3"/> <source>Flags</source> - <translation type="unfinished">Flags</translation> + <translation>Flags</translation> + </message> +</context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished">This Room</translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished">All Rooms</translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation>Unknown Message Type</translation> </message> </context> </TS> diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts new file mode 100644 index 00000000..208209c6 --- /dev/null +++ b/resources/langs/nheko_fi.ts @@ -0,0 +1,1369 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="fi"> +<context> + <name>ChatPage</name> + <message> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+399"/> + <source>Failed to restore OLM account. Please login again.</source> + <translation>OLM-tilin palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation> + </message> + <message> + <location line="+5"/> + <source>Failed to restore save data. Please login again.</source> + <translation>Tallennettujen tietojen palauttaminen epäonnistui. Ole hyvä ja kirjaudu sisään uudelleen.</translation> + </message> + <message> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation>Salausavainten lähetys epäonnistui. Palvelimen vastaus: %1 %2. Ole hyvä ja yritä uudelleen myöhemmin.</translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> + <source>Please try to login again: %1</source> + <translation>Ole hyvä ja yritä kirjautua sisään uudelleen: %1</translation> + </message> + <message> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Room creation failed: %1</source> + <translation>Huoneen luominen epäonnistui: %1</translation> + </message> + <message> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Failed to leave room: %1</source> + <translation>Huoneesta poistuminen epäonnistui: %1</translation> + </message> +</context> +<context> + <name>CommunitiesListItem</name> + <message> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation>Kaikki huoneet</translation> + </message> + <message> + <location line="+4"/> + <source>Favourite rooms</source> + <translation>Suosikkihuoneet</translation> + </message> + <message> + <location line="+2"/> + <source>Low priority rooms</source> + <translation>Alhaisen prioriteetin huoneet</translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation> (tag)</translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation> (community)</translation> + </message> +</context> +<context> + <name>EditModal</name> + <message> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> + <translation>Tallenna</translation> + </message> + <message> + <location line="+1"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+10"/> + <source>Name</source> + <translation>Nimi</translation> + </message> + <message> + <location line="+2"/> + <source>Topic</source> + <translation>Aihe</translation> + </message> +</context> +<context> + <name>EncryptionIndicator</name> + <message> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>InviteeItem</name> + <message> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation>Poista</translation> + </message> +</context> +<context> + <name>LoginPage</name> + <message> + <location filename="../../src/LoginPage.cpp" line="+82"/> + <source>Matrix ID</source> + <translation>Matrix-tunnus</translation> + </message> + <message> + <location line="+1"/> + <source>e.g @joe:matrix.org</source> + <translation>esim. @joe:matrix.org</translation> + </message> + <message> + <location line="+15"/> + <source>Password</source> + <translation>Salasana</translation> + </message> + <message> + <location line="+4"/> + <source>Device name</source> + <translation>Laitteen nimi</translation> + </message> + <message> + <location line="+19"/> + <source>LOGIN</source> + <translation>KIRJAUDU</translation> + </message> + <message> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation>Palvelimen tietojen hakeminen epäonnistui: virheellinen vastaus.</translation> + </message> + <message> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation>Palvelimen tietojen hakeminen epäonnistui: tuntematon virhe hakiessa .well-known -tiedostoa.</translation> + </message> + <message> + <location line="+24"/> + <source>The required endpoints were not found. Possibly not a Matrix server.</source> + <translation>Vaadittuja päätepisteitä ei löydetty. Mahdollisesti ei Matrix-palvelin.</translation> + </message> + <message> + <location line="+6"/> + <source>Received malformed response. Make sure the homeserver domain is valid.</source> + <translation>Vastaanotettiin virheellinen vastaus. Varmista, että kotipalvelimen osoite on pätevä.</translation> + </message> + <message> + <location line="+5"/> + <source>An unknown error occured. Make sure the homeserver domain is valid.</source> + <translation>Tapahtui tuntematon virhe. Varmista, että kotipalvelimen osoite on pätevä.</translation> + </message> + <message> + <location line="+60"/> + <source>Empty password</source> + <translation>Tyhjä salasana</translation> + </message> +</context> +<context> + <name>MemberList</name> + <message> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> + <source>Room members</source> + <translation>Huoneen jäsenet</translation> + </message> + <message> + <location line="+4"/> + <source>OK</source> + <translation>OK</translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>QuickSwitcher</name> + <message> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> + <source>Search for a room...</source> + <translation>Etsi huonetta…</translation> + </message> +</context> +<context> + <name>RegisterPage</name> + <message> + <location filename="../../src/RegisterPage.cpp" line="+80"/> + <source>Username</source> + <translation>Käyttäjänimi</translation> + </message> + <message> + <location line="+3"/> + <source>Password</source> + <translation>Salasana</translation> + </message> + <message> + <location line="+4"/> + <source>Password confirmation</source> + <translation>Salasanan varmistus</translation> + </message> + <message> + <location line="+4"/> + <source>Home Server</source> + <translation>Kotipalvelin</translation> + </message> + <message> + <location line="+16"/> + <source>REGISTER</source> + <translation>REKISTERÖIDY</translation> + </message> + <message> + <location line="+93"/> + <source>Invalid username</source> + <translation>Epäkelpo käyttäjänimi</translation> + </message> + <message> + <location line="+2"/> + <source>Password is not long enough (min 8 chars)</source> + <translation>Salasana ei ole tarpeeksi pitkä (vähintään 8 merkkiä)</translation> + </message> + <message> + <location line="+2"/> + <source>Passwords don't match</source> + <translation>Salasanat eivät täsmää</translation> + </message> + <message> + <location line="+2"/> + <source>Invalid server name</source> + <translation>Epäkelpo palvelimen nimi</translation> + </message> +</context> +<context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation>Kirjaudu ulos</translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation>ei tallennettua versiota</translation> + </message> +</context> +<context> + <name>RoomInfoListItem</name> + <message> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> + <source>Leave room</source> + <translation>Poistu huoneesta</translation> + </message> + <message> + <location line="+151"/> + <source>Accept</source> + <translation>Hyväksy</translation> + </message> + <message> + <location line="+3"/> + <source>Decline</source> + <translation>Hylkää</translation> + </message> +</context> +<context> + <name>SideBarActions</name> + <message> + <location filename="../../src/SideBarActions.cpp" line="+38"/> + <source>User settings</source> + <translation>Käyttäjäasetukset</translation> + </message> + <message> + <location line="+7"/> + <source>Create new room</source> + <translation>Luo uusi huone</translation> + </message> + <message> + <location line="+1"/> + <source>Join a room</source> + <translation>Liity huoneeseen</translation> + </message> + <message> + <location line="+16"/> + <source>Start a new chat</source> + <translation>Aloita uusi keskustelu</translation> + </message> + <message> + <location line="+15"/> + <source>Room directory</source> + <translation>Huoneluettelo</translation> + </message> +</context> +<context> + <name>StatusIndicator</name> + <message> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TextInputWidget</name> + <message> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> + <source>Send a file</source> + <translation>Lähetä tiedosto</translation> + </message> + <message> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> + <source>Write a message...</source> + <translation>Kirjoita viesti…</translation> + </message> + <message> + <location line="+31"/> + <source>Send a message</source> + <translation>Lähetä viesti</translation> + </message> + <message> + <location line="+8"/> + <source>Emoji</source> + <translation>Emoji</translation> + </message> + <message> + <location line="+85"/> + <source>Select a file</source> + <translation>Valitse tiedosto</translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation>Kaikki tiedostot (*)</translation> + </message> + <message> + <location filename="../../src/TextInputWidget.h" line="-5"/> + <source>Connection lost. Nheko is trying to re-connect...</source> + <translation>Yhteys kadotettu. Nheko yrittää muodostaa yhteyttä uudelleen…</translation> + </message> +</context> +<context> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished">-- Salattu viesti (salauksen purkuavaimia ei löydetty) --</translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished">-- Virhe purkaessa salausta (tietokannan kanssa kommunikointi epäonnistui) --</translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished">-- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epäonnistui) --</translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished">-- Virhe purkaessa salausta (%1) --</translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished">-- Salattu viesti (tuntematon viestityyppi) --</translation> + </message> + <message> + <location line="+47"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished">Viestin poisto epäonnistui: %1</translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">Tallenna kuva</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform>%1%2 kirjoittaa</numerusform> + <numerusform>%1 ja %2 kirjoittavat</numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">Lukukuittaukset</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished">Sulje</translation> + </message> +</context> +<context> + <name>TopRoomBar</name> + <message> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> + <source>Room options</source> + <translation>Huonevaihtoehdot</translation> + </message> + <message> + <location line="+5"/> + <source>Mentions</source> + <translation>Maininnat</translation> + </message> + <message> + <location line="+34"/> + <source>Invite users</source> + <translation>Kutsu käyttäjiä</translation> + </message> + <message> + <location line="+6"/> + <source>Members</source> + <translation>Jäsenet</translation> + </message> + <message> + <location line="+5"/> + <source>Leave room</source> + <translation>Poistu huoneesta</translation> + </message> + <message> + <location line="+5"/> + <source>Settings</source> + <translation>Asetukset</translation> + </message> +</context> +<context> + <name>TrayIcon</name> + <message> + <location filename="../../src/TrayIcon.cpp" line="+122"/> + <source>Show</source> + <translation>Näytä</translation> + </message> + <message> + <location line="+1"/> + <source>Quit</source> + <translation>Lopeta</translation> + </message> +</context> +<context> + <name>UserInfoWidget</name> + <message> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> + <source>Logout</source> + <translation>Kirjaudu ulos</translation> + </message> +</context> +<context> + <name>UserSettingsPage</name> + <message> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> + <source>Minimize to tray</source> + <translation>Pienennä ilmoitusalueelle</translation> + </message> + <message> + <location line="+3"/> + <source>Start in tray</source> + <translation>Aloita ilmoitusalueella</translation> + </message> + <message> + <location line="+5"/> + <source>Group's sidebar</source> + <translation>Ryhmäsivupalkki</translation> + </message> + <message> + <location line="+3"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Typing notifications</source> + <translation>Kirjoitusilmoitukset</translation> + </message> + <message> + <location line="+3"/> + <source>Read receipts</source> + <translation>Lukukuittaukset</translation> + </message> + <message> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Desktop notifications</source> + <translation>Työpöytäilmoitukset</translation> + </message> + <message> + <location line="+4"/> + <source>Scale factor</source> + <translation>Mittakerroin</translation> + </message> + <message> + <location line="+11"/> + <source>Font size</source> + <translation>Fonttikoko</translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation>Fonttiperhe</translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation>Emoji-fonttiperhe</translation> + </message> + <message> + <location line="+33"/> + <source>Theme</source> + <translation>Teema</translation> + </message> + <message> + <location line="+27"/> + <source>Device ID</source> + <translation>Laitteen tunnus</translation> + </message> + <message> + <location line="+12"/> + <source>Device Fingerprint</source> + <translation>Laitteen sormenjälki</translation> + </message> + <message> + <location line="+11"/> + <source>Session Keys</source> + <translation>Istunnon avaimet</translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation>TUO</translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation>VIE</translation> + </message> + <message> + <location line="+13"/> + <source>ENCRYPTION</source> + <translation>SALAUS</translation> + </message> + <message> + <location line="+4"/> + <source>GENERAL</source> + <translation>YLEISET ASETUKSET</translation> + </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation>Avaa Istuntoavaintiedosto</translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation>Virhe</translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation>Tiedoston Salasana</translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation>Anna salasana tiedoston salauksen purkamiseksi:</translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation>Salasana ei voi olla tyhjä</translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation>Anna salasana istuntoavaimien salaamiseksi:</translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation>Tiedosto, johon viedyt istuntoavaimet tallennetaan</translation> + </message> +</context> +<context> + <name>WelcomePage</name> + <message> + <location filename="../../src/WelcomePage.cpp" line="+47"/> + <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> + <translation>Tervetuloa nhekoon! Työpöytäsovellus Matrix-protokollalle.</translation> + </message> + <message> + <location line="+1"/> + <source>Enjoy your stay!</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+23"/> + <source>REGISTER</source> + <translation>REKISTERÖIDY</translation> + </message> + <message> + <location line="+5"/> + <source>LOGIN</source> + <translation>KIRJAUDU</translation> + </message> +</context> +<context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation>Eilen</translation> + </message> +</context> +<context> + <name>dialogs::CreateRoom</name> + <message> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation>Luo huone</translation> + </message> + <message> + <location line="+2"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+10"/> + <source>Name</source> + <translation>Nimi</translation> + </message> + <message> + <location line="+3"/> + <source>Topic</source> + <translation>Aihe</translation> + </message> + <message> + <location line="+3"/> + <source>Alias</source> + <translation>Osoite</translation> + </message> + <message> + <location line="+8"/> + <source>Room Visibility</source> + <translation>Huoneen näkyvyys</translation> + </message> + <message> + <location line="+8"/> + <source>Room Preset</source> + <translation>Huoneen esiasetus</translation> + </message> + <message> + <location line="+9"/> + <source>Direct Chat</source> + <translation>Suora keskustelu</translation> + </message> +</context> +<context> + <name>dialogs::InviteUsers</name> + <message> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+8"/> + <source>User ID to invite</source> + <translation>Käyttäjätunnus kutsuttavaksi</translation> + </message> +</context> +<context> + <name>dialogs::JoinRoom</name> + <message> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation>Liity</translation> + </message> + <message> + <location line="+2"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+7"/> + <source>Room ID or alias</source> + <translation>Huoneen tunnus tai osoite</translation> + </message> +</context> +<context> + <name>dialogs::LeaveRoom</name> + <message> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+8"/> + <source>Are you sure you want to leave?</source> + <translation>Oletko varma, että haluat poistua?</translation> + </message> +</context> +<context> + <name>dialogs::Logout</name> + <message> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+8"/> + <source>Logout. Are you sure?</source> + <translation>Kirjaudutaan ulos. Oletko varma?</translation> + </message> +</context> +<context> + <name>dialogs::PreviewUploadOverlay</name> + <message> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> + <source>Upload</source> + <translation>Lähetä</translation> + </message> + <message> + <location line="+1"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+84"/> + <source>Media type: %1 +Media size: %2 +</source> + <translation>Median tyyppi: %1 +Median koko: %2 +</translation> + </message> +</context> +<context> + <name>dialogs::ReCaptcha</name> + <message> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation>Peruuta</translation> + </message> + <message> + <location line="+1"/> + <source>Confirm</source> + <translation>Vahvista</translation> + </message> + <message> + <location line="+11"/> + <source>Solve the reCAPTCHA and press the confirm button</source> + <translation>Ratkaise reCAPTCHA ja paina varmista-nappia</translation> + </message> +</context> +<context> + <name>dialogs::ReadReceipts</name> + <message> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> + <source>Read receipts</source> + <translation>Lukukuittaukset</translation> + </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation>Sulje</translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation>Tänään %1</translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation>Eilen %1</translation> + </message> +</context> +<context> + <name>dialogs::RoomSettings</name> + <message> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> + <source>Settings</source> + <translation>Asetukset</translation> + </message> + <message> + <location line="+3"/> + <source>Info</source> + <translation>Tiedot</translation> + </message> + <message> + <location line="+11"/> + <source>Internal ID</source> + <translation>Sisäinen tunnus</translation> + </message> + <message> + <location line="+10"/> + <source>Room Version</source> + <translation>Huoneen versio</translation> + </message> + <message> + <location line="+4"/> + <source>Notifications</source> + <translation>Ilmoitukset</translation> + </message> + <message> + <location line="+2"/> + <source>Muted</source> + <translation>Vaimennettu</translation> + </message> + <message> + <location line="+2"/> + <source>Mentions only</source> + <translation>Vain maininnat</translation> + </message> + <message> + <location line="+1"/> + <source>All messages</source> + <translation>Kaikki viestit</translation> + </message> + <message> + <location line="+97"/> + <source>Room access</source> + <translation>Pääsy huoneeseen</translation> + </message> + <message> + <location line="+2"/> + <source>Anyone and guests</source> + <translation>Kaikki (mukaanlukien vieraat)</translation> + </message> + <message> + <location line="+1"/> + <source>Anyone</source> + <translation>Kaikki (poislukien vieraat)</translation> + </message> + <message> + <location line="+1"/> + <source>Invited users</source> + <translation>Kutsutut käyttäjät</translation> + </message> + <message> + <location line="+50"/> + <source>Encryption</source> + <translation>Salaus</translation> + </message> + <message> + <location line="+8"/> + <source>End-to-End Encryption</source> + <translation>Päästä-päähän-salaus</translation> + </message> + <message> + <location line="+1"/> + <source>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</source> + <translation>Salaus on tällä hetkellä kokeellinen ja asiat saattavat mennä rikki odottamattomasti.<br>Huomaa, ettei sitä voi poistaa käytöstä jälkikäteen.</translation> + </message> + <message> + <location line="+27"/> + <source>Respond to key requests</source> + <translation>Vastaa avainpyyntöihin</translation> + </message> + <message> + <location line="+3"/> + <source>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</source> + <translation>Pitäisikö asiakasohjelman palauttaa salausavaimet automaattisesti pyydettäessä. + Käytä varoen, tämä on väliaikainen vaihtoehto salausjärjestelmän testausta varten, + kunnes laitteiden vahvistus on valmis.</translation> + </message> + <message numerus="yes"> + <location line="+51"/> + <source>%n member(s)</source> + <translation> + <numerusform>%n käyttäjä</numerusform> + <numerusform>%n käyttäjää</numerusform> + </translation> + </message> + <message> + <location line="+140"/> + <source>Failed to enable encryption: %1</source> + <translation>Salauksen aktivointi epäonnistui: %1</translation> + </message> + <message> + <location line="+145"/> + <source>Select an avatar</source> + <translation>Valitse profiilikuva</translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation>Kaikki Tiedostot (*)</translation> + </message> + <message> + <location line="+12"/> + <source>The selected file is not an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Error while reading file: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+35"/> + <location line="+20"/> + <source>Failed to upload image: %s</source> + <translation>Kuvan lähetys epäonnistui: %s</translation> + </message> +</context> +<context> + <name>dialogs::UserProfile</name> + <message> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> + <source>Ban the user from the room</source> + <translation>Anna käyttäjälle porttikielto huoneesta</translation> + </message> + <message> + <location line="+8"/> + <source>Ignore messages from this user</source> + <translation>Jätä tämän käyttäjän viestit huomiotta</translation> + </message> + <message> + <location line="+9"/> + <source>Kick the user from the room</source> + <translation>Potki käyttäjä huoneesta</translation> + </message> + <message> + <location line="+8"/> + <source>Start a conversation</source> + <translation>Aloita keskustelu</translation> + </message> + <message> + <location line="+63"/> + <source>Devices</source> + <translation>Laitteet</translation> + </message> +</context> +<context> + <name>emoji::Panel</name> + <message> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> + <source>Smileys & People</source> + <translation>Hymiöt ja ihmiset</translation> + </message> + <message> + <location line="+4"/> + <source>Animals & Nature</source> + <translation>Eläimet ja luonto</translation> + </message> + <message> + <location line="+3"/> + <source>Food & Drink</source> + <translation>Ruoka ja juoma</translation> + </message> + <message> + <location line="+3"/> + <source>Activity</source> + <translation>Aktiviteetti</translation> + </message> + <message> + <location line="+4"/> + <source>Travel & Places</source> + <translation>Matkailu ja paikat</translation> + </message> + <message> + <location line="+3"/> + <source>Objects</source> + <translation>Esineet</translation> + </message> + <message> + <location line="+3"/> + <source>Symbols</source> + <translation>Symbolit</translation> + </message> + <message> + <location line="+3"/> + <source>Flags</source> + <translation>Liput</translation> + </message> +</context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index 4b995d07..6c97cc64 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -2,37 +2,150 @@ <!DOCTYPE TS> <TS version="2.1" language="fr"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cc" line="+125"/> - <source>Save File</source> - <translation>Enregistrer le fichier</translation> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+399"/> + <source>Failed to restore OLM account. Please login again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Failed to restore save data. Please login again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> + <source>Please try to login again: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Room creation failed: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Failed to leave room: %1</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>DateSeparator</name> + <name>CommunitiesListItem</name> + <message> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/timeline/TimelineView.cc" line="+54"/> - <source>Today</source> - <translation>Aujourd'hui</translation> + <location line="+4"/> + <source>Favourite rooms</source> + <translation type="unfinished"></translation> </message> <message> <location line="+2"/> - <source>Yesterday</source> - <translation>Hier</translation> + <source>Low priority rooms</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>EditModal</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+36"/> - <source>APPLY</source> - <translation>APPLIQUER</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location line="+1"/> + <source>Cancel</source> + <translation type="unfinished">Annuler</translation> </message> <message> <location line="+10"/> @@ -46,25 +159,25 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cc" line="+111"/> - <source>Save File</source> - <translation>Enregistrer le fichier</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>ImageItem</name> + <name>InviteeItem</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cc" line="+229"/> - <source>Save image</source> - <translation>Enregistrer l'image</translation> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cc" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> <translation>Identifiant Matrix</translation> </message> @@ -79,56 +192,104 @@ <translation>Mot de passe</translation> </message> <message> + <location line="+4"/> + <source>Device name</source> + <translation type="unfinished"></translation> + </message> + <message> <location line="+19"/> <source>LOGIN</source> <translation>CONNEXION</translation> </message> <message> - <location line="+128"/> - <source>Empty password</source> - <translation>Mot de passe vide</translation> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation type="unfinished"></translation> </message> -</context> -<context> - <name>MatrixClient</name> <message> - <location filename="../../src/MatrixClient.cc" line="+164"/> - <source>Wrong username or password</source> - <translation>Mauvais nom d'utilisateur ou mot de passe</translation> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+5"/> - <source>Login endpoint was not found on the server</source> - <translation>L'interface de connexion n'a pas pu être trouvée sur le serveur</translation> + <location line="+24"/> + <source>The required endpoints were not found. Possibly not a Matrix server.</source> + <translation type="unfinished"></translation> </message> <message> <location line="+6"/> - <source>An unknown error occured. Please try again.</source> - <translation>Une erreur inconnue s'est produite. Veuillez essayer à nouveau.</translation> + <source>Received malformed response. Make sure the homeserver domain is valid.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+23"/> - <source>Malformed response. Possibly not a Matrix server</source> - <translation>La réponse du serveur est malformée. Il est possible qu'il ne s'agisse pas d'un serveur Matrix</translation> + <location line="+5"/> + <source>An unknown error occured. Make sure the homeserver domain is valid.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+60"/> + <source>Empty password</source> + <translation>Mot de passe vide</translation> </message> </context> <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+79"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> <source>Room members</source> <translation>Membres du salon</translation> </message> <message> - <location line="+10"/> - <source>SHOW MORE</source> - <translation>MONTRER PLUS</translation> + <location line="+4"/> + <source>OK</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cc" line="+70"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> <translation>Chercher un salon…</translation> </message> @@ -136,7 +297,7 @@ <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cc" line="+76"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> <translation>Nom d'utilisateur</translation> </message> @@ -157,12 +318,12 @@ <translation>Serveur Matrix</translation> </message> <message> - <location line="+17"/> + <location line="+16"/> <source>REGISTER</source> <translation>S'ENREGISTRER</translation> </message> <message> - <location line="+76"/> + <location line="+93"/> <source>Invalid username</source> <translation>Nom d'utilisateur invalide</translation> </message> @@ -183,19 +344,35 @@ </message> </context> <context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cc" line="+78"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Quitter le salon</translation> </message> <message> - <location line="+153"/> + <location line="+151"/> <source>Accept</source> <translation>Accepter</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> <translation>Décliner</translation> </message> @@ -203,7 +380,12 @@ <context> <name>SideBarActions</name> <message> - <location filename="../../src/SideBarActions.cc" line="+36"/> + <location filename="../../src/SideBarActions.cpp" line="+38"/> + <source>User settings</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> <source>Create new room</source> <translation>Créer un nouveau salon</translation> </message> @@ -212,16 +394,65 @@ <source>Join a room</source> <translation>Rejoindre un salon</translation> </message> + <message> + <location line="+16"/> + <source>Start a new chat</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>Room directory</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>StatusIndicator</name> + <message> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cc" line="+445"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> + <source>Send a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> <translation>Écrivez un message...</translation> </message> <message> - <location line="+108"/> + <location line="+31"/> + <source>Send a message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Emoji</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+85"/> <source>Select a file</source> <translation>Sélectionnez un fichier</translation> </message> @@ -230,11 +461,220 @@ <source>All Files (*)</source> <translation>Tous les types de fichiers (*)</translation> </message> + <message> + <location filename="../../src/TextInputWidget.h" line="-5"/> + <source>Connection lost. Nheko is trying to re-connect...</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+47"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">Enregistrer l'image</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">Accusés de lecture</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cc" line="+87"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> + <source>Room options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Mentions</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> <translation>Inviter des utilisateurs</translation> </message> @@ -257,7 +697,7 @@ <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cc" line="+116"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> <translation>Montrer</translation> </message> @@ -268,70 +708,166 @@ </message> </context> <context> - <name>TypingDisplay</name> - <message> - <location filename="../../src/TypingDisplay.cc" line="+26"/> - <source> is typing</source> - <translation> est en train d'écrire</translation> - </message> + <name>UserInfoWidget</name> <message> - <location line="+2"/> - <source> are typing</source> - <translation> sont en train d'écrire</translation> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> + <source>Logout</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cc" line="+121"/> - <source>User Settings</source> - <translation>Paramètres utilisateur</translation> - </message> - <message> - <location line="+15"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> <translation>Réduire à la barre des tâches</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> <translation>Démarrer dans la barre des tâches</translation> </message> <message> - <location line="+12"/> - <source>Re-order rooms based on activity</source> - <translation>Ré-ordonner les salons en fonction de leur activité</translation> - </message> - <message> - <location line="+9"/> + <location line="+5"/> <source>Group's sidebar</source> <translation>Barre latérale des groupes</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Typing notifications</source> <translation>Notifications d'écriture</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> <translation>Accusés de lecture</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Desktop notifications</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Scale factor</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Font size</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> <translation>Thème</translation> </message> <message> - <location line="+10"/> + <location line="+27"/> + <source>Device ID</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Device Fingerprint</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Session Keys</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>ENCRYPTION</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> <source>GENERAL</source> <translation>GÉNÉRAL</translation> </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cc" line="+44"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> <translation>Bienvenue sur nheko ! Le client de bureau pour le protocole Matrix.</translation> </message> @@ -341,25 +877,38 @@ <translation>Bon séjour !</translation> </message> <message> - <location line="+19"/> + <location line="+23"/> <source>REGISTER</source> <translation>S'ENREGISTRER</translation> </message> <message> - <location line="+6"/> + <location line="+5"/> <source>LOGIN</source> <translation>CONNEXION</translation> </message> </context> <context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>dialogs::CreateRoom</name> <message> - <location filename="../../src/dialogs/CreateRoom.cc" line="+32"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Annuler</translation> + </message> + <message> + <location line="+10"/> <source>Name</source> <translation>Nom</translation> </message> @@ -379,12 +928,12 @@ <translation>Visibilité du salon</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Room Preset</source> <translation>Préréglage du salon</translation> </message> <message> - <location line="+10"/> + <location line="+9"/> <source>Direct Chat</source> <translation>Discussion directe</translation> </message> @@ -392,12 +941,12 @@ <context> <name>dialogs::InviteUsers</name> <message> - <location filename="../../src/dialogs/InviteUsers.cc" line="+36"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation type="unfinished">Annuler</translation> </message> <message> - <location line="+11"/> + <location line="+8"/> <source>User ID to invite</source> <translation>Identifiant d'utilisateur à inviter</translation> </message> @@ -405,12 +954,17 @@ <context> <name>dialogs::JoinRoom</name> <message> - <location filename="../../src/dialogs/JoinRoom.cc" line="+30"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Annuler</translation> + </message> + <message> + <location line="+7"/> <source>Room ID or alias</source> <translation>Identifiant ou alias du salon</translation> </message> @@ -418,12 +972,12 @@ <context> <name>dialogs::LeaveRoom</name> <message> - <location filename="../../src/dialogs/LeaveRoom.cc" line="+29"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Annuler</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Are you sure you want to leave?</source> <translation>Êtes-vous sûr·e de vouloir quitter ?</translation> </message> @@ -431,12 +985,12 @@ <context> <name>dialogs::Logout</name> <message> - <location filename="../../src/dialogs/Logout.cc" line="+47"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation type="unfinished">Annuler</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Logout. Are you sure?</source> <translation>Déconnexion. Êtes-vous sûr·e ?</translation> </message> @@ -444,7 +998,7 @@ <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cc" line="+41"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> <translation>Envoyer</translation> </message> @@ -454,7 +1008,7 @@ <translation>Annuler</translation> </message> <message> - <location line="+72"/> + <location line="+84"/> <source>Media type: %1 Media size: %2 </source> @@ -466,14 +1020,14 @@ Taille du média : %2 <context> <name>dialogs::ReCaptcha</name> <message> - <location filename="../../src/dialogs/ReCaptcha.cpp" line="+34"/> - <source>CONFIRM</source> - <translation>CONFIRMER</translation> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Annuler</translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location line="+1"/> + <source>Confirm</source> + <translation type="unfinished"></translation> </message> <message> <location line="+11"/> @@ -484,30 +1038,63 @@ Taille du média : %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cc" line="+98"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Accusés de lecture</translation> </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>dialogs::RoomSettings</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+144"/> - <source>CANCEL</source> - <translation>ANNULER</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> + <source>Settings</source> + <translation type="unfinished">Paramètres</translation> </message> <message> - <location line="+12"/> + <location line="+3"/> + <source>Info</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Internal ID</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+10"/> + <source>Room Version</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> <source>Notifications</source> <translation>Notifications</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> <translation>Silencieux</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> <translation>Uniquement les mentions</translation> </message> @@ -517,7 +1104,7 @@ Taille du média : %2 <translation>Tous les messages</translation> </message> <message> - <location line="+8"/> + <location line="+97"/> <source>Room access</source> <translation>Accès au salon</translation> </message> @@ -536,11 +1123,105 @@ Taille du média : %2 <source>Invited users</source> <translation>Utilisateurs invités</translation> </message> + <message> + <location line="+50"/> + <source>Encryption</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>End-to-End Encryption</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+27"/> + <source>Respond to key requests</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+51"/> + <source>%n member(s)</source> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+140"/> + <source>Failed to enable encryption: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+145"/> + <source>Select an avatar</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation type="unfinished">Tous les types de fichiers (*)</translation> + </message> + <message> + <location line="+12"/> + <source>The selected file is not an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Error while reading file: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+35"/> + <location line="+20"/> + <source>Failed to upload image: %s</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::UserProfile</name> + <message> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> + <source>Ban the user from the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Ignore messages from this user</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>Kick the user from the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Start a conversation</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+63"/> + <source>Devices</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>emoji::Panel</name> <message> - <location filename="../../src/emoji/Panel.cc" line="+125"/> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> <source>Smileys & People</source> <translation>Smileys & Personnes</translation> </message> @@ -580,4 +1261,108 @@ Taille du média : %2 <translation>Drapeaux</translation> </message> </context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation type="unfinished"></translation> + </message> +</context> </TS> diff --git a/resources/langs/nheko_ja.ts b/resources/langs/nheko_ja.ts new file mode 100644 index 00000000..180c060e --- /dev/null +++ b/resources/langs/nheko_ja.ts @@ -0,0 +1,1368 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="ja_JP"> +<context> + <name>ChatPage</name> + <message> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation>ユーザーを招待できませんでした: %1</translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation>招待されたユーザー: %1</translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation>%2に%1を招待できませんでした: %3</translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation>%2に%1を一時的に追放できませんでした: %3</translation> + </message> + <message> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation>一時的に追放されたユーザー: %1</translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation>%2で%1を永久追放できませんでした: %3</translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation>永久追放されたユーザー: %1</translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation>%2で%1の永久追放を解除できませんでした: %3</translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation>永久追放を解除されたユーザー: %1</translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation>メディアをアップロードできませんでした。やり直して下さい。</translation> + </message> + <message> + <location line="+399"/> + <source>Failed to restore OLM account. Please login again.</source> + <translation>OLMアカウントを復元できませんでした。もう一度ログインして下さい。</translation> + </message> + <message> + <location line="+5"/> + <source>Failed to restore save data. Please login again.</source> + <translation>セーブデータを復元できませんでした。もう一度ログインして下さい。</translation> + </message> + <message> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation>暗号化鍵を設定できませんでした。サーバーの応答: %1 %2. 後でやり直して下さい。</translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> + <source>Please try to login again: %1</source> + <translation>もう一度ログインしてみて下さい: %1</translation> + </message> + <message> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation>部屋に参加できませんでした: %1</translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation>部屋に参加しました</translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation>招待を削除できませんでした: %1</translation> + </message> + <message> + <location line="+19"/> + <source>Room creation failed: %1</source> + <translation>部屋を作成できませんでした: %1</translation> + </message> + <message> + <location line="+5"/> + <source>Room %1 created</source> + <translation>部屋 %1 を作成しました</translation> + </message> + <message> + <location line="+11"/> + <source>Failed to leave room: %1</source> + <translation>部屋から出られませんでした: %1</translation> + </message> +</context> +<context> + <name>CommunitiesListItem</name> + <message> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation>全ての部屋</translation> + </message> + <message> + <location line="+4"/> + <source>Favourite rooms</source> + <translation>お気に入りの部屋</translation> + </message> + <message> + <location line="+2"/> + <source>Low priority rooms</source> + <translation>優先度の低い部屋</translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation> (タグ)</translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation> (コミュニティー)</translation> + </message> +</context> +<context> + <name>EditModal</name> + <message> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> + <translation>適用</translation> + </message> + <message> + <location line="+1"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+10"/> + <source>Name</source> + <translation>名前</translation> + </message> + <message> + <location line="+2"/> + <source>Topic</source> + <translation>話題</translation> + </message> +</context> +<context> + <name>EncryptionIndicator</name> + <message> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation>暗号化されています</translation> + </message> +</context> +<context> + <name>InviteeItem</name> + <message> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation>削除</translation> + </message> +</context> +<context> + <name>LoginPage</name> + <message> + <location filename="../../src/LoginPage.cpp" line="+82"/> + <source>Matrix ID</source> + <translation>Matrix ID</translation> + </message> + <message> + <location line="+1"/> + <source>e.g @joe:matrix.org</source> + <translation>例 @joe:matrix.org</translation> + </message> + <message> + <location line="+15"/> + <source>Password</source> + <translation>パスワード</translation> + </message> + <message> + <location line="+4"/> + <source>Device name</source> + <translation>デバイス名</translation> + </message> + <message> + <location line="+19"/> + <source>LOGIN</source> + <translation>ログイン</translation> + </message> + <message> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation>自動検出できませんでした。不正な形式の応答を受信しました。</translation> + </message> + <message> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation>自動検出できませんでした。.well-known要求時の不明なエラー。</translation> + </message> + <message> + <location line="+24"/> + <source>The required endpoints were not found. Possibly not a Matrix server.</source> + <translation>必要な端点が見つかりません。Matrixサーバーではないかもしれません。</translation> + </message> + <message> + <location line="+6"/> + <source>Received malformed response. Make sure the homeserver domain is valid.</source> + <translation>不正な形式の応答を受信しました。ホームサーバーのドメイン名が有効であるかを確認して下さい。</translation> + </message> + <message> + <location line="+5"/> + <source>An unknown error occured. Make sure the homeserver domain is valid.</source> + <translation>不明なエラーが発生しました。ホームサーバーのドメイン名が有効であるかを確認して下さい。</translation> + </message> + <message> + <location line="+60"/> + <source>Empty password</source> + <translation>パスワードが入力されていません</translation> + </message> +</context> +<context> + <name>MemberList</name> + <message> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> + <source>Room members</source> + <translation>部屋の参加者</translation> + </message> + <message> + <location line="+4"/> + <source>OK</source> + <translation>OK</translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation>編集済み</translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation>暗号化が有効です</translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation>部屋名が変更されました: %1</translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation>部屋名が削除されました</translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation>話題が変更されました: %1</translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation>話題が削除されました</translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation>未実装のイベント: </translation> + </message> +</context> +<context> + <name>QuickSwitcher</name> + <message> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> + <source>Search for a room...</source> + <translation>部屋を探す...</translation> + </message> +</context> +<context> + <name>RegisterPage</name> + <message> + <location filename="../../src/RegisterPage.cpp" line="+80"/> + <source>Username</source> + <translation>ユーザー名</translation> + </message> + <message> + <location line="+3"/> + <source>Password</source> + <translation>パスワード</translation> + </message> + <message> + <location line="+4"/> + <source>Password confirmation</source> + <translation>パスワード確認</translation> + </message> + <message> + <location line="+4"/> + <source>Home Server</source> + <translation>ホームサーバー</translation> + </message> + <message> + <location line="+16"/> + <source>REGISTER</source> + <translation>登録</translation> + </message> + <message> + <location line="+93"/> + <source>Invalid username</source> + <translation>無効なユーザー名です</translation> + </message> + <message> + <location line="+2"/> + <source>Password is not long enough (min 8 chars)</source> + <translation>パスワード長が不足しています (最小8文字)</translation> + </message> + <message> + <location line="+2"/> + <source>Passwords don't match</source> + <translation>パスワードが一致しません</translation> + </message> + <message> + <location line="+2"/> + <source>Invalid server name</source> + <translation>無効なサーバー名です</translation> + </message> +</context> +<context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation>ログアウト</translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation>バージョンが保存されていません</translation> + </message> +</context> +<context> + <name>RoomInfoListItem</name> + <message> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> + <source>Leave room</source> + <translation>部屋を出る</translation> + </message> + <message> + <location line="+151"/> + <source>Accept</source> + <translation>容認</translation> + </message> + <message> + <location line="+3"/> + <source>Decline</source> + <translation>拒否</translation> + </message> +</context> +<context> + <name>SideBarActions</name> + <message> + <location filename="../../src/SideBarActions.cpp" line="+38"/> + <source>User settings</source> + <translation>ユーザー設定</translation> + </message> + <message> + <location line="+7"/> + <source>Create new room</source> + <translation>新しい部屋を作成</translation> + </message> + <message> + <location line="+1"/> + <source>Join a room</source> + <translation>部屋に参加</translation> + </message> + <message> + <location line="+16"/> + <source>Start a new chat</source> + <translation>新しいチャットを開始</translation> + </message> + <message> + <location line="+15"/> + <source>Room directory</source> + <translation>部屋一覧</translation> + </message> +</context> +<context> + <name>StatusIndicator</name> + <message> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation>失敗</translation> + </message> + <message> + <location line="+1"/> + <source>Sent</source> + <translation>送信済み</translation> + </message> + <message> + <location line="+1"/> + <source>Received</source> + <translation>受信済み</translation> + </message> + <message> + <location line="+1"/> + <source>Read</source> + <translation>既読</translation> + </message> +</context> +<context> + <name>TextInputWidget</name> + <message> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> + <source>Send a file</source> + <translation>ファイルを送信</translation> + </message> + <message> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> + <source>Write a message...</source> + <translation>メッセージを書く...</translation> + </message> + <message> + <location line="+31"/> + <source>Send a message</source> + <translation>メッセージを送信</translation> + </message> + <message> + <location line="+8"/> + <source>Emoji</source> + <translation>絵文字</translation> + </message> + <message> + <location line="+85"/> + <source>Select a file</source> + <translation>ファイルを選択</translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation>全てのファイル (*)</translation> + </message> + <message> + <location filename="../../src/TextInputWidget.h" line="-5"/> + <source>Connection lost. Nheko is trying to re-connect...</source> + <translation>接続が切れました。Nhekoは再接続を試みています...</translation> + </message> +</context> +<context> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation>-- 暗号化イベント (復号鍵が見つかりません) --</translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation>-- 復号エラー (データベースと通信できませんでした) --</translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation>-- 復号エラー (データベースからmegolm鍵を取得できませんでした) --</translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation>-- 復号エラー (%1) --</translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation>-- 暗号化イベント (不明なイベント型です) --</translation> + </message> + <message> + <location line="+47"/> + <source>Message redaction failed: %1</source> + <translation>メッセージを編集できませんでした: %1</translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation>画像を保存</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation>動画を保存</translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation>音声を保存</translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation>ファイルを保存</translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation> + <numerusform>%1%2が入力しています</numerusform> + <numerusform>%1と%2が入力しています</numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation>%1が招待されました。</translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation>%1が表示名とアバターを変更しました。</translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation>%1が表示名を変更しました。</translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation>%1がアバターを変更しました。</translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation>%1が参加しました。</translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation>%1が招待を拒否しました。</translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation>%1への招待を取り消しました。</translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation>%1は退室しました。</translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation>%1を一時的に追放しました。</translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation>%1の永久追放を解除しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation>%1がノックを編集しました。</translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation>%1からのノックを拒否しました。</translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation>退出済みの%1が退出しました!</translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation>%1が永久追放されました。</translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation>%1がノックしました。</translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation>返信</translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation>オプション</translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation>開封確認</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation>既読にする</translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation>ソースを見る</translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation>メッセージを編集</translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation>名前を付けて保存</translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation>部屋が開いていません</translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation>閉じる</translation> + </message> +</context> +<context> + <name>TopRoomBar</name> + <message> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> + <source>Room options</source> + <translation>部屋のオプション</translation> + </message> + <message> + <location line="+5"/> + <source>Mentions</source> + <translation>メンション</translation> + </message> + <message> + <location line="+34"/> + <source>Invite users</source> + <translation>ユーザーを招待</translation> + </message> + <message> + <location line="+6"/> + <source>Members</source> + <translation>メンバー</translation> + </message> + <message> + <location line="+5"/> + <source>Leave room</source> + <translation>退室する</translation> + </message> + <message> + <location line="+5"/> + <source>Settings</source> + <translation>設定</translation> + </message> +</context> +<context> + <name>TrayIcon</name> + <message> + <location filename="../../src/TrayIcon.cpp" line="+122"/> + <source>Show</source> + <translation>表示</translation> + </message> + <message> + <location line="+1"/> + <source>Quit</source> + <translation>終了</translation> + </message> +</context> +<context> + <name>UserInfoWidget</name> + <message> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> + <source>Logout</source> + <translation>ログアウト</translation> + </message> +</context> +<context> + <name>UserSettingsPage</name> + <message> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> + <source>Minimize to tray</source> + <translation>トレイへ最小化</translation> + </message> + <message> + <location line="+3"/> + <source>Start in tray</source> + <translation>トレイで起動</translation> + </message> + <message> + <location line="+5"/> + <source>Group's sidebar</source> + <translation>グループサイドバー</translation> + </message> + <message> + <location line="+3"/> + <source>Circular Avatars</source> + <translation>円形アバター</translation> + </message> + <message> + <location line="+3"/> + <source>Typing notifications</source> + <translation>入力状態の通知</translation> + </message> + <message> + <location line="+3"/> + <source>Read receipts</source> + <translation>開封確認</translation> + </message> + <message> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation>メッセージをMarkdownとして送信</translation> + </message> + <message> + <location line="+3"/> + <source>Desktop notifications</source> + <translation>デスクトップ通知</translation> + </message> + <message> + <location line="+4"/> + <source>Scale factor</source> + <translation>尺度係数</translation> + </message> + <message> + <location line="+11"/> + <source>Font size</source> + <translation>フォントサイズ</translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation>フォントファミリー</translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation>絵文字のフォントファミリー</translation> + </message> + <message> + <location line="+33"/> + <source>Theme</source> + <translation>テーマ</translation> + </message> + <message> + <location line="+27"/> + <source>Device ID</source> + <translation>デバイスID</translation> + </message> + <message> + <location line="+12"/> + <source>Device Fingerprint</source> + <translation>デバイスの指紋</translation> + </message> + <message> + <location line="+11"/> + <source>Session Keys</source> + <translation>セッション鍵</translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation>インポート</translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation>エクスポート</translation> + </message> + <message> + <location line="+13"/> + <source>ENCRYPTION</source> + <translation>暗号化</translation> + </message> + <message> + <location line="+4"/> + <source>GENERAL</source> + <translation>全般</translation> + </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation>セッションファイルを開く</translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation>エラー</translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation>ファイルのパスワード</translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation>ファイルを復号するためのパスフレーズを入力して下さい:</translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation>パスワードを空にはできません</translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation>セッション鍵を暗号化するためのパスフレーズを入力して下さい:</translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation>エクスポートされたセッション鍵を保存するファイル</translation> + </message> +</context> +<context> + <name>WelcomePage</name> + <message> + <location filename="../../src/WelcomePage.cpp" line="+47"/> + <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> + <translation>Matrixプロトコルのデスクトップクライアント、nhekoへようこそ!</translation> + </message> + <message> + <location line="+1"/> + <source>Enjoy your stay!</source> + <translation>会話を楽しんで下さい!</translation> + </message> + <message> + <location line="+23"/> + <source>REGISTER</source> + <translation>登録</translation> + </message> + <message> + <location line="+5"/> + <source>LOGIN</source> + <translation>ログイン</translation> + </message> +</context> +<context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation>昨日</translation> + </message> +</context> +<context> + <name>dialogs::CreateRoom</name> + <message> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation>部屋を作成</translation> + </message> + <message> + <location line="+2"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+10"/> + <source>Name</source> + <translation>名前</translation> + </message> + <message> + <location line="+3"/> + <source>Topic</source> + <translation>話題</translation> + </message> + <message> + <location line="+3"/> + <source>Alias</source> + <translation>別名</translation> + </message> + <message> + <location line="+8"/> + <source>Room Visibility</source> + <translation>部屋の可視性</translation> + </message> + <message> + <location line="+8"/> + <source>Room Preset</source> + <translation>部屋の初期値</translation> + </message> + <message> + <location line="+9"/> + <source>Direct Chat</source> + <translation>ダイレクトメッセージ</translation> + </message> +</context> +<context> + <name>dialogs::InviteUsers</name> + <message> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+8"/> + <source>User ID to invite</source> + <translation>招待するユーザーのID</translation> + </message> +</context> +<context> + <name>dialogs::JoinRoom</name> + <message> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation>参加</translation> + </message> + <message> + <location line="+2"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+7"/> + <source>Room ID or alias</source> + <translation>部屋のID又は別名</translation> + </message> +</context> +<context> + <name>dialogs::LeaveRoom</name> + <message> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+8"/> + <source>Are you sure you want to leave?</source> + <translation>本当に退出しますか?</translation> + </message> +</context> +<context> + <name>dialogs::Logout</name> + <message> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+8"/> + <source>Logout. Are you sure?</source> + <translation>本当にログアウトしますか?</translation> + </message> +</context> +<context> + <name>dialogs::PreviewUploadOverlay</name> + <message> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> + <source>Upload</source> + <translation>アップロード</translation> + </message> + <message> + <location line="+1"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+84"/> + <source>Media type: %1 +Media size: %2 +</source> + <translation>メディアの種類: %1 +メディアのサイズ: %2 +</translation> + </message> +</context> +<context> + <name>dialogs::ReCaptcha</name> + <message> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation>キャンセル</translation> + </message> + <message> + <location line="+1"/> + <source>Confirm</source> + <translation>確認</translation> + </message> + <message> + <location line="+11"/> + <source>Solve the reCAPTCHA and press the confirm button</source> + <translation>reCAPTCHAに解答して、確認ボタンを押して下さい</translation> + </message> +</context> +<context> + <name>dialogs::ReadReceipts</name> + <message> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> + <source>Read receipts</source> + <translation>開封確認</translation> + </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation>閉じる</translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation>今日 %1</translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation>昨日 %1</translation> + </message> +</context> +<context> + <name>dialogs::RoomSettings</name> + <message> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> + <source>Settings</source> + <translation>設定</translation> + </message> + <message> + <location line="+3"/> + <source>Info</source> + <translation>情報</translation> + </message> + <message> + <location line="+11"/> + <source>Internal ID</source> + <translation>内部ID</translation> + </message> + <message> + <location line="+10"/> + <source>Room Version</source> + <translation>部屋のバージョン</translation> + </message> + <message> + <location line="+4"/> + <source>Notifications</source> + <translation>通知</translation> + </message> + <message> + <location line="+2"/> + <source>Muted</source> + <translation>ミュート</translation> + </message> + <message> + <location line="+2"/> + <source>Mentions only</source> + <translation>メンションのみ</translation> + </message> + <message> + <location line="+1"/> + <source>All messages</source> + <translation>全てのメッセージ</translation> + </message> + <message> + <location line="+97"/> + <source>Room access</source> + <translation>部屋のアクセス権</translation> + </message> + <message> + <location line="+2"/> + <source>Anyone and guests</source> + <translation>登録ユーザーとゲスト</translation> + </message> + <message> + <location line="+1"/> + <source>Anyone</source> + <translation>登録ユーザーのみ</translation> + </message> + <message> + <location line="+1"/> + <source>Invited users</source> + <translation>招待された登録ユーザーのみ</translation> + </message> + <message> + <location line="+50"/> + <source>Encryption</source> + <translation>暗号化</translation> + </message> + <message> + <location line="+8"/> + <source>End-to-End Encryption</source> + <translation>エンドツーエンド暗号化</translation> + </message> + <message> + <location line="+1"/> + <source>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</source> + <translation>暗号化機能は実験段階にあるので、予期せずに壊れるかもしれません。 <br>この機能を後から無効にできないことに注意して下さい。</translation> + </message> + <message> + <location line="+27"/> + <source>Respond to key requests</source> + <translation>鍵の要求に応答する</translation> + </message> + <message> + <location line="+3"/> + <source>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</source> + <translation>クライアントがセッション鍵を要求された際に、自動的に応答するべきか否か。 +デバイス検証機能が実装されるまでのE2E暗号化を検査するための一時的な方法なので、 +これを利用する際は注意して下さい。</translation> + </message> + <message numerus="yes"> + <location line="+51"/> + <source>%n member(s)</source> + <translation> + <numerusform>%n人</numerusform> + </translation> + </message> + <message> + <location line="+140"/> + <source>Failed to enable encryption: %1</source> + <translation>暗号化を有効にできませんでした: %1</translation> + </message> + <message> + <location line="+145"/> + <source>Select an avatar</source> + <translation>アバターを選択</translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation>全てのファイル (*)</translation> + </message> + <message> + <location line="+12"/> + <source>The selected file is not an image</source> + <translation>選択したファイルは画像ではありません</translation> + </message> + <message> + <location line="+5"/> + <source>Error while reading file: %1</source> + <translation>ファイルの読み込み時にエラーが発生しました: %1</translation> + </message> + <message> + <location line="+35"/> + <location line="+20"/> + <source>Failed to upload image: %s</source> + <translation>画像をアップロードできませんでした: %s</translation> + </message> +</context> +<context> + <name>dialogs::UserProfile</name> + <message> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> + <source>Ban the user from the room</source> + <translation>ユーザーを部屋から永久追放する</translation> + </message> + <message> + <location line="+8"/> + <source>Ignore messages from this user</source> + <translation>このユーザーからのメッセージを無視する</translation> + </message> + <message> + <location line="+9"/> + <source>Kick the user from the room</source> + <translation>ユーザーを部屋から一時的に追放する</translation> + </message> + <message> + <location line="+8"/> + <source>Start a conversation</source> + <translation>会話を始める</translation> + </message> + <message> + <location line="+63"/> + <source>Devices</source> + <translation>デバイス</translation> + </message> +</context> +<context> + <name>emoji::Panel</name> + <message> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> + <source>Smileys & People</source> + <translation>表情と人</translation> + </message> + <message> + <location line="+4"/> + <source>Animals & Nature</source> + <translation>動物と自然</translation> + </message> + <message> + <location line="+3"/> + <source>Food & Drink</source> + <translation>飲食物</translation> + </message> + <message> + <location line="+3"/> + <source>Activity</source> + <translation>活動</translation> + </message> + <message> + <location line="+4"/> + <source>Travel & Places</source> + <translation>旅行と場所</translation> + </message> + <message> + <location line="+3"/> + <source>Objects</source> + <translation>物</translation> + </message> + <message> + <location line="+3"/> + <source>Symbols</source> + <translation>記号</translation> + </message> + <message> + <location line="+3"/> + <source>Flags</source> + <translation>旗</translation> + </message> +</context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation>音声データを送信しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation>%1が音声データを送信しました</translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation>画像を送信しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation>%1が画像を送信しました</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation>ファイルを送信しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation>%1がファイルを送信しました</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation>動画を送信しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation>%1が動画を送信しました</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation>ステッカーを送信しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation>%1がステッカーを送信しました</translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation>通知を送信しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation>%1が通知を送信しました</translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation>あなた: %1</translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation>%1: %2</translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation>暗号化されたメッセージを送信しました</translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation>%1が暗号化されたメッセージを送信しました</translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation>この部屋</translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation>全ての部屋</translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation>不明なメッセージ型です</translation> + </message> +</context> +</TS> diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index c13ab123..dba70d50 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -2,40 +2,153 @@ <!DOCTYPE TS> <TS version="2.1" language="nl_NL"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cc" line="+125"/> - <source>Save File</source> - <translation>Bestand opslaan</translation> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+399"/> + <source>Failed to restore OLM account. Please login again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Failed to restore save data. Please login again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> + <source>Please try to login again: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Room creation failed: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Failed to leave room: %1</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>DateSeparator</name> + <name>CommunitiesListItem</name> + <message> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/timeline/TimelineView.cc" line="+54"/> - <source>Today</source> - <translation>Vandaag</translation> + <location line="+4"/> + <source>Favourite rooms</source> + <translation type="unfinished"></translation> </message> <message> <location line="+2"/> - <source>Yesterday</source> - <translation>Gisteren</translation> + <source>Low priority rooms</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>EditModal</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+34"/> - <source>APPLY</source> - <translation>TOEPASSEN</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+2"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location line="+1"/> + <source>Cancel</source> + <translation type="unfinished">Annuleren</translation> </message> <message> - <location line="+9"/> + <location line="+10"/> <source>Name</source> <translation>Naam</translation> </message> @@ -46,25 +159,25 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cc" line="+111"/> - <source>Save File</source> - <translation>Bestand opslaan</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>ImageItem</name> + <name>InviteeItem</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cc" line="+229"/> - <source>Save image</source> - <translation>Afbeelding opslaan</translation> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cc" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> <translation>Matrix-id</translation> </message> @@ -79,56 +192,104 @@ <translation>Wachtwoord</translation> </message> <message> + <location line="+4"/> + <source>Device name</source> + <translation type="unfinished"></translation> + </message> + <message> <location line="+19"/> <source>LOGIN</source> <translation>INLOGGEN</translation> </message> <message> - <location line="+128"/> - <source>Empty password</source> - <translation>Leeg wachtwoord</translation> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation type="unfinished"></translation> </message> -</context> -<context> - <name>MatrixClient</name> <message> - <location filename="../../src/MatrixClient.cc" line="+164"/> - <source>Wrong username or password</source> - <translation>Verkeerde gebruikersnaam of wachtwoord</translation> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+5"/> - <source>Login endpoint was not found on the server</source> - <translation>Het inlog-endpoint is niet aangetroffen op de server</translation> + <location line="+24"/> + <source>The required endpoints were not found. Possibly not a Matrix server.</source> + <translation type="unfinished"></translation> </message> <message> <location line="+6"/> - <source>An unknown error occured. Please try again.</source> - <translation>Er is een onbekende fout opgetreden. Probeer het opnieuw.</translation> + <source>Received malformed response. Make sure the homeserver domain is valid.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+23"/> - <source>Malformed response. Possibly not a Matrix server</source> - <translation>Onjuist antwoord ontvangen: dit is mogelijk geen Matrix server</translation> + <location line="+5"/> + <source>An unknown error occured. Make sure the homeserver domain is valid.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+60"/> + <source>Empty password</source> + <translation>Leeg wachtwoord</translation> </message> </context> <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+79"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> <source>Room members</source> <translation>Kamerleden</translation> </message> <message> - <location line="+10"/> - <source>SHOW MORE</source> - <translation>MEER TONEN</translation> + <location line="+4"/> + <source>OK</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cc" line="+70"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> <translation>Zoek een kamer...</translation> </message> @@ -136,7 +297,7 @@ <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cc" line="+76"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> <translation>Gebruikersnaam</translation> </message> @@ -156,12 +317,12 @@ <translation>Thuisserver</translation> </message> <message> - <location line="+17"/> + <location line="+16"/> <source>REGISTER</source> <translation>REGISTREREN</translation> </message> <message> - <location line="+76"/> + <location line="+93"/> <source>Invalid username</source> <translation>Ongeldige gebruikersnaam</translation> </message> @@ -182,19 +343,35 @@ </message> </context> <context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cc" line="+78"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Kamer verlaten</translation> </message> <message> - <location line="+153"/> + <location line="+151"/> <source>Accept</source> <translation>Accepteren</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> <translation>Afwijzen</translation> </message> @@ -202,7 +379,12 @@ <context> <name>SideBarActions</name> <message> - <location filename="../../src/SideBarActions.cc" line="+36"/> + <location filename="../../src/SideBarActions.cpp" line="+38"/> + <source>User settings</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> <source>Create new room</source> <translation>Nieuwe kamer creëren</translation> </message> @@ -211,16 +393,65 @@ <source>Join a room</source> <translation>Kamer betreden</translation> </message> + <message> + <location line="+16"/> + <source>Start a new chat</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>Room directory</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>StatusIndicator</name> + <message> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cc" line="+445"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> + <source>Send a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> <translation>Typ een bericht...</translation> </message> <message> - <location line="+108"/> + <location line="+31"/> + <source>Send a message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Emoji</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+85"/> <source>Select a file</source> <translation>Kies een bestand</translation> </message> @@ -229,11 +460,220 @@ <source>All Files (*)</source> <translation>Alle bestanden (*)</translation> </message> + <message> + <location filename="../../src/TextInputWidget.h" line="-5"/> + <source>Connection lost. Nheko is trying to re-connect...</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+47"/> + <source>Message redaction failed: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">Afbeelding opslaan</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineView</name> + <message> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">Leesbevestigingen</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cc" line="+87"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> + <source>Room options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Mentions</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> <translation>Gebruikers uitnodigen</translation> </message> @@ -256,7 +696,7 @@ <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cc" line="+116"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> <translation>Tonen</translation> </message> @@ -267,70 +707,166 @@ </message> </context> <context> - <name>TypingDisplay</name> - <message> - <location filename="../../src/TypingDisplay.cc" line="+26"/> - <source> is typing</source> - <translation> is aan het typen</translation> - </message> + <name>UserInfoWidget</name> <message> - <location line="+2"/> - <source> are typing</source> - <translation> zijn aan het typen</translation> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> + <source>Logout</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cc" line="+121"/> - <source>User Settings</source> - <translation>Gebruikersinstellingen</translation> - </message> - <message> - <location line="+15"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> <translation>Minimaliseren naar systeemvak</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> <translation>Geminimaliseerd opstarten</translation> </message> <message> - <location line="+12"/> - <source>Re-order rooms based on activity</source> - <translation>Kamers herordenen op basis van activiteit</translation> - </message> - <message> - <location line="+9"/> + <location line="+5"/> <source>Group's sidebar</source> <translation>Zijbalk van groep</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Typing notifications</source> <translation>Meldingen bij typen van berichten</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> <translation>Leesbevestigingen</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Desktop notifications</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Scale factor</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Font size</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> <translation>Thema</translation> </message> <message> - <location line="+10"/> + <location line="+27"/> + <source>Device ID</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>Device Fingerprint</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> + <source>Session Keys</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>ENCRYPTION</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> <source>GENERAL</source> <translation>ALGEMEEN</translation> </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cc" line="+44"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> <translation>Welkom bij nheko! Dé computerclient voor het Matrix-protocol.</translation> </message> @@ -340,25 +876,38 @@ <translation>Geniet van je verblijf!</translation> </message> <message> - <location line="+19"/> + <location line="+23"/> <source>REGISTER</source> <translation>REGISTREREN</translation> </message> <message> - <location line="+6"/> + <location line="+5"/> <source>LOGIN</source> <translation>INLOGGEN</translation> </message> </context> <context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>dialogs::CreateRoom</name> <message> - <location filename="../../src/dialogs/CreateRoom.cc" line="+32"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Annuleren</translation> + </message> + <message> + <location line="+10"/> <source>Name</source> <translation>Naam</translation> </message> @@ -378,12 +927,12 @@ <translation>Kamerzichtbaarheid</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Room Preset</source> <translation>Kamer-voorinstellingen</translation> </message> <message> - <location line="+10"/> + <location line="+9"/> <source>Direct Chat</source> <translation>Directe chat</translation> </message> @@ -391,12 +940,12 @@ <context> <name>dialogs::InviteUsers</name> <message> - <location filename="../../src/dialogs/InviteUsers.cc" line="+36"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation type="unfinished">Annuleren</translation> </message> <message> - <location line="+11"/> + <location line="+8"/> <source>User ID to invite</source> <translation>Uit te nodigen gebruikers-id</translation> </message> @@ -404,12 +953,17 @@ <context> <name>dialogs::JoinRoom</name> <message> - <location filename="../../src/dialogs/JoinRoom.cc" line="+30"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Annuleren</translation> + </message> + <message> + <location line="+7"/> <source>Room ID or alias</source> <translation>Kamer-id of alias</translation> </message> @@ -417,12 +971,12 @@ <context> <name>dialogs::LeaveRoom</name> <message> - <location filename="../../src/dialogs/LeaveRoom.cc" line="+29"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Annuleren</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Are you sure you want to leave?</source> <translation>Weet je zeker dat je wilt vertrekken?</translation> </message> @@ -430,12 +984,12 @@ <context> <name>dialogs::Logout</name> <message> - <location filename="../../src/dialogs/Logout.cc" line="+47"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation type="unfinished">Annuleren</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Logout. Are you sure?</source> <translation>Uitloggen. Weet je het zeker?</translation> </message> @@ -443,7 +997,7 @@ <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cc" line="+41"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> <translation>Uploaden</translation> </message> @@ -453,7 +1007,7 @@ <translation>Annuleren</translation> </message> <message> - <location line="+72"/> + <location line="+84"/> <source>Media type: %1 Media size: %2 </source> @@ -465,14 +1019,14 @@ Mediagrootte: %2 <context> <name>dialogs::ReCaptcha</name> <message> - <location filename="../../src/dialogs/ReCaptcha.cpp" line="+34"/> - <source>CONFIRM</source> - <translation>BEVESTIGEN</translation> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Annuleren</translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location line="+1"/> + <source>Confirm</source> + <translation type="unfinished"></translation> </message> <message> <location line="+11"/> @@ -483,30 +1037,63 @@ Mediagrootte: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cc" line="+98"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Leesbevestigingen</translation> </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>dialogs::RoomSettings</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+147"/> - <source>CANCEL</source> - <translation>ANNULEREN</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> + <source>Settings</source> + <translation type="unfinished">Instellingen</translation> + </message> + <message> + <location line="+3"/> + <source>Info</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+12"/> + <location line="+11"/> + <source>Internal ID</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+10"/> + <source>Room Version</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> <source>Notifications</source> <translation>Meldingen</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> <translation>Gedempt</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> <translation>Alleen vermeldingen</translation> </message> @@ -516,7 +1103,7 @@ Mediagrootte: %2 <translation>Alle berichten</translation> </message> <message> - <location line="+8"/> + <location line="+97"/> <source>Room access</source> <translation>Kamertoegang</translation> </message> @@ -535,11 +1122,105 @@ Mediagrootte: %2 <source>Invited users</source> <translation>Uitgenodigde gebruikers</translation> </message> + <message> + <location line="+50"/> + <source>Encryption</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>End-to-End Encryption</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+27"/> + <source>Respond to key requests</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed.</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+51"/> + <source>%n member(s)</source> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+140"/> + <source>Failed to enable encryption: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+145"/> + <source>Select an avatar</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>All Files (*)</source> + <translation type="unfinished">Alle bestanden (*)</translation> + </message> + <message> + <location line="+12"/> + <source>The selected file is not an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Error while reading file: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+35"/> + <location line="+20"/> + <source>Failed to upload image: %s</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::UserProfile</name> + <message> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> + <source>Ban the user from the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Ignore messages from this user</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>Kick the user from the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <source>Start a conversation</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+63"/> + <source>Devices</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>emoji::Panel</name> <message> - <location filename="../../src/emoji/Panel.cc" line="+125"/> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> <source>Smileys & People</source> <translation>Smileys en mensen</translation> </message> @@ -579,4 +1260,108 @@ Mediagrootte: %2 <translation>Vlaggen</translation> </message> </context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation type="unfinished"></translation> + </message> +</context> </TS> diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index 2d908653..fd23b216 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -2,37 +2,60 @@ <!DOCTYPE TS> <TS version="2.1" language="pl"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+118"/> - <source>Save File</source> - <translation>Zapisz plik</translation> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> </message> -</context> -<context> - <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+303"/> - <source>Failed to upload image. Please try again.</source> - <translation>Nie udało się wysłać obrazu. Spróbuj ponownie.</translation> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>Nie udało się wysłać pliku. Spróbuj ponownie.</translation> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>Nie udało się wysłać pliku dźwiękowego. Spróbuj ponownie.</translation> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>Nie udało się wysłać filmu. Spróbuj ponownie.</translation> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+366"/> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+399"/> <source>Failed to restore OLM account. Please login again.</source> <translation>Nie udało się przywrócić konta OLM. Spróbuj zalogować się ponownie.</translation> </message> @@ -42,49 +65,90 @@ <translation>Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie.</translation> </message> <message> - <location line="+212"/> - <location line="+150"/> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+51"/> + <location line="+231"/> <source>Please try to login again: %1</source> <translation>Spróbuj zalogować się ponownie: %1</translation> </message> <message> - <location line="-45"/> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> <source>Room creation failed: %1</source> <translation>Tworzenie pokoju nie powiodło się: %1</translation> </message> <message> - <location line="+16"/> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> <source>Failed to leave room: %1</source> <translation>Nie udało się opuścić pokoju: %1</translation> </message> </context> <context> - <name>DateSeparator</name> + <name>CommunitiesListItem</name> <message> - <location filename="../../src/ui/InfoMessage.cpp" line="+68"/> - <source>Today</source> - <translation>Dzisiaj</translation> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Favourite rooms</source> + <translation type="unfinished"></translation> </message> <message> <location line="+2"/> - <source>Yesterday</source> - <translation>Wczoraj</translation> + <source>Low priority rooms</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <location line="+2"/> + <source> (tag)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>EditModal</name> <message> <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> - <source>APPLY</source> - <translation>ZASTOSUJ</translation> + <source>Apply</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+5"/> - <source>CANCEL</source> - <translation>ANULUJ</translation> + <location line="+1"/> + <source>Cancel</source> + <translation type="unfinished">Anuluj</translation> </message> <message> - <location line="+12"/> + <location line="+10"/> <source>Name</source> <translation>Nazwa</translation> </message> @@ -95,25 +159,25 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+108"/> - <source>Save File</source> - <translation>Zapisz plik</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>ImageItem</name> + <name>InviteeItem</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+236"/> - <source>Save image</source> - <translation>Zapisz obraz</translation> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cpp" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> <translation>ID Matrixa</translation> </message> @@ -138,7 +202,17 @@ <translation>ZALOGUJ</translation> </message> <message> - <location line="+85"/> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+24"/> <source>The required endpoints were not found. Possibly not a Matrix server.</source> <translation>Nie odnaleziono wymaganych punktów końcowych. To może nie być serwer Matriksa.</translation> </message> @@ -161,20 +235,61 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+82"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> <source>Room members</source> <translation>Członkowie pokoju</translation> </message> <message> - <location line="+10"/> - <source>SHOW MORE</source> - <translation>POKAŻ WIĘCEJ</translation> + <location line="+4"/> + <source>OK</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cpp" line="+71"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> <translation>Wyszukaj pokoju…</translation> </message> @@ -182,7 +297,7 @@ <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cpp" line="+77"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> <translation>Nazwa użytkownika</translation> </message> @@ -202,7 +317,7 @@ <translation>Serwer domowy</translation> </message> <message> - <location line="+17"/> + <location line="+16"/> <source>REGISTER</source> <translation>ZAREJESTRUJ</translation> </message> @@ -228,19 +343,35 @@ </message> </context> <context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation type="unfinished">Wyloguj</translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+77"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Opuść pokój</translation> </message> <message> - <location line="+153"/> + <location line="+151"/> <source>Accept</source> <translation>Akceptuj</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> <translation>Odrzuć</translation> </message> @@ -248,7 +379,7 @@ <context> <name>SideBarActions</name> <message> - <location filename="../../src/SideBarActions.cpp" line="+32"/> + <location filename="../../src/SideBarActions.cpp" line="+38"/> <source>User settings</source> <translation>Ustawienia użytkownika</translation> </message> @@ -276,41 +407,41 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+126"/> - <source>Encrypted</source> - <translation>Szyfrowana</translation> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>Dostarczono</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>Wyświetlona</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>Wysłana</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+452"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> <source>Send a file</source> <translation>Wyślij plik</translation> </message> <message> - <location line="+17"/> - <location filename="../../src/TextInputWidget.h" line="+154"/> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> <translation>Napisz wiadomość…</translation> </message> <message> - <location line="+27"/> + <location line="+31"/> <source>Send a message</source> <translation>Wyślij wiadomość</translation> </message> @@ -320,7 +451,7 @@ <translation>Emoji</translation> </message> <message> - <location line="+77"/> + <location line="+85"/> <source>Select a file</source> <translation>Wybierz plik</translation> </message> @@ -336,30 +467,214 @@ </message> </context> <context> - <name>TimelineItem</name> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+76"/> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+47"/> <source>Message redaction failed: %1</source> - <translation>Redagowanie wiadomości nie powiodło się: %1</translation> + <translation type="unfinished">Redagowanie wiadomości nie powiodło się: %1</translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">Zapisz obraz</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TimelineView</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>Szyfrowanie jest włączone</translation> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">Potwierdzenia przeczytania</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+68"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> <source>Room options</source> <translation>Ustawienia pokoju</translation> </message> <message> - <location line="+28"/> + <location line="+5"/> + <source>Mentions</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> <translation>Zaproś użytkowników</translation> </message> @@ -382,7 +697,7 @@ <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cpp" line="+116"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> <translation>Pokaż</translation> </message> @@ -393,22 +708,9 @@ </message> </context> <context> - <name>TypingDisplay</name> - <message> - <location filename="../../src/TypingDisplay.cpp" line="+49"/> - <source> is typing</source> - <translation> pisze</translation> - </message> - <message> - <location line="+2"/> - <source> are typing</source> - <translation> piszą</translation> - </message> -</context> -<context> <name>UserInfoWidget</name> <message> - <location filename="../../src/UserInfoWidget.cpp" line="+78"/> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> <source>Logout</source> <translation>Wyloguj</translation> </message> @@ -416,75 +718,156 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+140"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> <translation>Zminimalizuj do paska zadań</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> <translation>Rozpocznij na pasku zadań</translation> </message> <message> - <location line="+12"/> - <source>Re-order rooms based on activity</source> - <translation>Porządkuj pokoje na podstawie aktywności</translation> - </message> - <message> - <location line="+9"/> + <location line="+5"/> <source>Group's sidebar</source> <translation>Pasek boczny grupy</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Typing notifications</source> <translation>Powiadomienia o pisaniu</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> <translation>Potwierdzenia przeczytania</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Desktop notifications</source> <translation>Powiadomienia na pulpicie</translation> </message> <message> - <location line="+9"/> - <source>Scale factor (requires restart)</source> - <translation>Czynnik skalowania (wymaga ponownego uruchomienia)</translation> + <location line="+4"/> + <source>Scale factor</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+18"/> + <location line="+11"/> + <source>Font size</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> <translation>Motyw</translation> </message> <message> - <location line="+21"/> + <location line="+27"/> <source>Device ID</source> <translation>ID urządzenia</translation> </message> <message> - <location line="+11"/> + <location line="+12"/> <source>Device Fingerprint</source> <translation>Odcisk palca urządzenia</translation> </message> <message> + <location line="+11"/> + <source>Session Keys</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>IMPORT</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>EXPORT</source> + <translation type="unfinished"></translation> + </message> + <message> <location line="+13"/> <source>ENCRYPTION</source> <translation>SZYFROWANIE</translation> </message> <message> - <location line="+5"/> + <location line="+4"/> <source>GENERAL</source> <translation>OGÓLNE</translation> </message> + <message> + <location line="+168"/> + <source>Open Sessions File</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+18"/> + <location line="+9"/> + <location line="+2"/> + <location line="+2"/> + <location line="+19"/> + <location line="+10"/> + <location line="+18"/> + <location line="+2"/> + <location line="+2"/> + <source>Error</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-73"/> + <location line="+32"/> + <source>File Password</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-31"/> + <source>Enter the passphrase to decrypt the file:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+8"/> + <location line="+32"/> + <source>The password cannot be empty</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-8"/> + <source>Enter passphrase to encrypt your session keys:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>File to save the exported session keys</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cpp" line="+44"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> <translation>Witamy w nheko! Desktopowy klient protokołu Matrix.</translation> </message> @@ -494,25 +877,38 @@ <translation>Udanego pobytu!</translation> </message> <message> - <location line="+19"/> + <location line="+23"/> <source>REGISTER</source> <translation>ZAREJESTRUJ SIĘ</translation> </message> <message> - <location line="+6"/> + <location line="+5"/> <source>LOGIN</source> <translation>ZALOGUJ SIĘ</translation> </message> </context> <context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>dialogs::CreateRoom</name> <message> - <location filename="../../src/dialogs/CreateRoom.cpp" line="+41"/> - <source>CANCEL</source> - <translation>ANULUJ</translation> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Anuluj</translation> + </message> + <message> + <location line="+10"/> <source>Name</source> <translation>Nazwa</translation> </message> @@ -545,9 +941,9 @@ <context> <name>dialogs::InviteUsers</name> <message> - <location filename="../../src/dialogs/InviteUsers.cpp" line="+40"/> - <source>CANCEL</source> - <translation>ANULUJ</translation> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation type="unfinished">Anuluj</translation> </message> <message> <location line="+8"/> @@ -558,12 +954,17 @@ <context> <name>dialogs::JoinRoom</name> <message> - <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/> - <source>CANCEL</source> - <translation>ANULUJ</translation> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+8"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">Anuluj</translation> + </message> + <message> + <location line="+7"/> <source>Room ID or alias</source> <translation>ID pokoju lub alias</translation> </message> @@ -571,12 +972,12 @@ <context> <name>dialogs::LeaveRoom</name> <message> - <location filename="../../src/dialogs/LeaveRoom.cpp" line="+33"/> - <source>CANCEL</source> - <translation>ANULUJ</translation> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Anuluj</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Are you sure you want to leave?</source> <translation>Czy na pewno chcesz wyjść?</translation> </message> @@ -584,12 +985,12 @@ <context> <name>dialogs::Logout</name> <message> - <location filename="../../src/dialogs/Logout.cpp" line="+52"/> - <source>CANCEL</source> - <translation>ANULUJ</translation> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation type="unfinished">Anuluj</translation> </message> <message> - <location line="+11"/> + <location line="+8"/> <source>Logout. Are you sure?</source> <translation>Czy na pewno chcesz wylogować się?</translation> </message> @@ -597,7 +998,7 @@ <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+42"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> <translation>Wyślij</translation> </message> @@ -607,7 +1008,7 @@ <translation>Anuluj</translation> </message> <message> - <location line="+89"/> + <location line="+84"/> <source>Media type: %1 Media size: %2 </source> @@ -619,14 +1020,14 @@ Rozmiar multimediów: %2 <context> <name>dialogs::ReCaptcha</name> <message> - <location filename="../../src/dialogs/ReCaptcha.cpp" line="+38"/> - <source>CONFIRM</source> - <translation>POTWIERDŹ</translation> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">Anuluj</translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation>ANULUJ</translation> + <location line="+1"/> + <source>Confirm</source> + <translation type="unfinished"></translation> </message> <message> <location line="+11"/> @@ -637,15 +1038,33 @@ Rozmiar multimediów: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+104"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Potwierdzenia przeczytania</translation> </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>dialogs::RoomSettings</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+116"/> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> <source>Settings</source> <translation>Ustawienia</translation> </message> @@ -655,22 +1074,27 @@ Rozmiar multimediów: %2 <translation>Informacje</translation> </message> <message> - <location line="+13"/> + <location line="+11"/> <source>Internal ID</source> <translation>Wewnętrzne ID</translation> </message> <message> + <location line="+10"/> + <source>Room Version</source> + <translation type="unfinished"></translation> + </message> + <message> <location line="+4"/> <source>Notifications</source> <translation>Powiadomienia</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> <translation>Wyciszone</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> <translation>Tylko wspomnienia</translation> </message> @@ -680,7 +1104,7 @@ Rozmiar multimediów: %2 <translation>Wszystkie wiadomości</translation> </message> <message> - <location line="+7"/> + <location line="+97"/> <source>Room access</source> <translation>Dostęp do pokoju</translation> </message> @@ -729,7 +1153,7 @@ Rozmiar multimediów: %2 do testowania implementacji E2E, zanim weryfikacja urządzeń będzie ukończona.</translation> </message> <message numerus="yes"> - <location line="+53"/> + <location line="+51"/> <source>%n member(s)</source> <translation> <numerusform>%n członek</numerusform> @@ -738,12 +1162,12 @@ Rozmiar multimediów: %2 </translation> </message> <message> - <location line="+125"/> + <location line="+140"/> <source>Failed to enable encryption: %1</source> <translation>Nie udało się włączyć szyfrowania: %1</translation> </message> <message> - <location line="+158"/> + <location line="+145"/> <source>Select an avatar</source> <translation>Wybierz awatar</translation> </message> @@ -754,13 +1178,13 @@ Rozmiar multimediów: %2 </message> <message> <location line="+12"/> - <source>The selected media is not an image</source> - <translation>Wybrany plik multimedialny nie jest obrazem</translation> + <source>The selected file is not an image</source> + <translation type="unfinished"></translation> </message> <message> <location line="+5"/> - <source>Error while reading media: %1</source> - <translation>Błąd odczytywania pliku: %1</translation> + <source>Error while reading file: %1</source> + <translation type="unfinished"></translation> </message> <message> <location line="+35"/> @@ -772,12 +1196,12 @@ Rozmiar multimediów: %2 <context> <name>dialogs::UserProfile</name> <message> - <location filename="../../src/dialogs/UserProfile.cpp" line="+59"/> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> <source>Ban the user from the room</source> <translation>Zablokuj użytkownika w tym pokoju</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Ignore messages from this user</source> <translation>Ignoruj wiadomości od tego użytkownika</translation> </message> @@ -787,12 +1211,12 @@ Rozmiar multimediów: %2 <translation>Wyrzuć użytkownika z tego pokoju</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Start a conversation</source> <translation>Rozpocznij rozmowę</translation> </message> <message> - <location line="+56"/> + <location line="+63"/> <source>Devices</source> <translation>Urządzenia</translation> </message> @@ -800,7 +1224,7 @@ Rozmiar multimediów: %2 <context> <name>emoji::Panel</name> <message> - <location filename="../../src/emoji/Panel.cpp" line="+125"/> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> <source>Smileys & People</source> <translation>Twarze i ludzie</translation> </message> @@ -840,4 +1264,108 @@ Rozmiar multimediów: %2 <translation>Flagi</translation> </message> </context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation type="unfinished"></translation> + </message> +</context> </TS> diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 297611ab..e3817db0 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -2,37 +2,60 @@ <!DOCTYPE TS> <TS version="2.1" language="ru"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+117"/> - <source>Save File</source> - <translation>Сохранить файл</translation> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> </message> -</context> -<context> - <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+304"/> - <source>Failed to upload image. Please try again.</source> - <translation>Не удалось загрузить изображение. Пожалуйста, попробуйте еще раз.</translation> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>Не удалось загрузить файл. Пожалуйста, попробуйте еще раз.</translation> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>Не удалось загрузить аудио. Пожалуйста, попробуйте еще раз.</translation> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>Не удалось загрузить видео. Пожалуйста, попробуйте еще раз.</translation> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+374"/> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+399"/> <source>Failed to restore OLM account. Please login again.</source> <translation>Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова.</translation> </message> @@ -42,23 +65,43 @@ <translation>Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова.</translation> </message> <message> - <location line="+167"/> + <location line="+148"/> <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> <translation>Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже.</translation> </message> <message> <location line="+51"/> - <location line="+152"/> + <location line="+231"/> <source>Please try to login again: %1</source> <translation>Повторите попытку входа: %1</translation> </message> <message> - <location line="-45"/> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> <source>Room creation failed: %1</source> <translation>Не удалось создать комнату: %1</translation> </message> <message> - <location line="+16"/> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> <source>Failed to leave room: %1</source> <translation>Не удалось покинуть комнату: %1</translation> </message> @@ -66,7 +109,7 @@ <context> <name>CommunitiesListItem</name> <message> - <location filename="../../src/CommunitiesListItem.cpp" line="+130"/> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> <source>All rooms</source> <translation>Все комнаты</translation> </message> @@ -95,7 +138,7 @@ <context> <name>EditModal</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+58"/> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> <source>Apply</source> <translation>Применить</translation> </message> @@ -116,19 +159,11 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+106"/> - <source>Save File</source> - <translation>Сохранить файл</translation> - </message> -</context> -<context> - <name>ImageItem</name> - <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+237"/> - <source>Save image</source> - <translation>Сохранить изображение</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -142,7 +177,7 @@ <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cpp" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> <translation>Идентификатор Matrix</translation> </message> @@ -168,6 +203,16 @@ </message> <message> <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+24"/> <source>The required endpoints were not found. Possibly not a Matrix server.</source> <translation>Необходимые конечные точки не найдены. Возможно, это не сервер Matrix.</translation> </message> @@ -190,20 +235,61 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+96"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> <source>Room members</source> <translation>Участники комнаты</translation> </message> <message> - <location line="+33"/> - <source>ESC</source> - <translation></translation> + <location line="+4"/> + <source>OK</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cpp" line="+71"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> <translation>Поиск комнаты...</translation> </message> @@ -211,7 +297,7 @@ <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cpp" line="+77"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> <translation>Имя пользователя</translation> </message> @@ -257,19 +343,35 @@ </message> </context> <context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation type="unfinished">Выйти</translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+92"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>Покинуть комнату</translation> </message> <message> - <location line="+166"/> + <location line="+151"/> <source>Accept</source> <translation>Принять</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> <translation>Отказаться</translation> </message> @@ -305,36 +407,36 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+105"/> - <source>Encrypted</source> - <translation>Зашифровано</translation> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>Доставлено</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>Прочитано</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>Отправлено</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+465"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> <source>Send a file</source> <translation>Отправить файл</translation> </message> <message> <location line="+13"/> - <location filename="../../src/TextInputWidget.h" line="+153"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> <translation>Написать сообщение...</translation> </message> @@ -344,7 +446,12 @@ <translation>Отправить сообщение</translation> </message> <message> - <location line="+47"/> + <location line="+8"/> + <source>Emoji</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+85"/> <source>Select a file</source> <translation>Выберите файл</translation> </message> @@ -360,30 +467,214 @@ </message> </context> <context> - <name>TimelineItem</name> + <name>TimelineModel</name> + <message> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+72"/> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+47"/> <source>Message redaction failed: %1</source> - <translation>Ошибка редактирования сообщения: %1</translation> + <translation type="unfinished">Ошибка редактирования сообщения: %1</translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">Сохранить изображение</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TimelineView</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>Шифрование включено</translation> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">Подтверждать прочтение</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished">Закрыть</translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+79"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> <source>Room options</source> <translation>Настройки комнаты</translation> </message> <message> - <location line="+28"/> + <location line="+5"/> + <source>Mentions</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> <translation>Пригласить пользователей</translation> </message> @@ -406,7 +697,7 @@ <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cpp" line="+120"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> <translation>Показать</translation> </message> @@ -417,22 +708,9 @@ </message> </context> <context> - <name>TypingDisplay</name> - <message> - <location filename="../../src/TypingDisplay.cpp" line="+45"/> - <source> is typing</source> - <translation> печатает</translation> - </message> - <message> - <location line="+2"/> - <source> are typing</source> - <translation> печатают</translation> - </message> -</context> -<context> <name>UserInfoWidget</name> <message> - <location filename="../../src/UserInfoWidget.cpp" line="+87"/> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> <source>Logout</source> <translation>Выйти</translation> </message> @@ -440,37 +718,47 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+147"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> <translation>Сворачивать в системную панель</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> <translation>Запускать в системной панели</translation> </message> <message> - <location line="+11"/> + <location line="+5"/> <source>Group's sidebar</source> <translation>Боковая панель групп</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Typing notifications</source> <translation>Сообщать о наборе сообщения</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> <translation>Подтверждать прочтение</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Desktop notifications</source> <translation>Уведомления на рабочем столе</translation> </message> <message> - <location line="+9"/> + <location line="+4"/> <source>Scale factor</source> <translation>Масштаб</translation> </message> @@ -480,12 +768,22 @@ <translation>Размер шрифта</translation> </message> <message> - <location line="+11"/> + <location line="+13"/> + <source>Font Family</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> <translation>Тема</translation> </message> <message> - <location line="+22"/> + <location line="+27"/> <source>Device ID</source> <translation>ID устройства</translation> </message> @@ -520,7 +818,7 @@ <translation>ГЛАВНОЕ</translation> </message> <message> - <location line="+144"/> + <location line="+168"/> <source>Open Sessions File</source> <translation>Открыть файл сеансов</translation> </message> @@ -532,14 +830,14 @@ <location line="+2"/> <location line="+19"/> <location line="+10"/> - <location line="+13"/> + <location line="+18"/> <location line="+2"/> <location line="+2"/> <source>Error</source> <translation>Ошибка</translation> </message> <message> - <location line="-68"/> + <location line="-73"/> <location line="+32"/> <source>File Password</source> <translatorcomment>Или введите пароль?</translatorcomment> @@ -570,7 +868,7 @@ <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cpp" line="+46"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> <translation>Добро пожаловать в nheko, клиент протокола Matrix!</translation> </message> @@ -591,6 +889,14 @@ </message> </context> <context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>dialogs::CreateRoom</name> <message> <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> @@ -693,7 +999,7 @@ <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+42"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> <translation>Загрузить</translation> </message> @@ -733,7 +1039,7 @@ Media size: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+119"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>Подтверждать прочтение</translation> </message> @@ -742,10 +1048,18 @@ Media size: %2 <source>Close</source> <translation>Закрыть</translation> </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> <message> - <location line="+11"/> - <source>ESC</source> - <translation></translation> + <location line="-44"/> + <source>Today %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation type="unfinished"></translation> </message> </context> <context> @@ -766,17 +1080,22 @@ Media size: %2 <translation>Внутренний ID</translation> </message> <message> + <location line="+10"/> + <source>Room Version</source> + <translation type="unfinished"></translation> + </message> + <message> <location line="+4"/> <source>Notifications</source> <translation>Уведомления</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> <translation>Приглушено</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> <translation>Только упоминания</translation> </message> @@ -786,7 +1105,7 @@ Media size: %2 <translation>Все сообщения</translation> </message> <message> - <location line="+7"/> + <location line="+97"/> <source>Room access</source> <translation>Доступ к комнате</translation> </message> @@ -833,7 +1152,7 @@ Media size: %2 <translation type="unfinished"></translation> </message> <message numerus="yes"> - <location line="+53"/> + <location line="+51"/> <source>%n member(s)</source> <translation> <numerusform>%n участник</numerusform> @@ -842,17 +1161,12 @@ Media size: %2 </translation> </message> <message> - <location line="+70"/> - <source>ESC</source> - <translation></translation> - </message> - <message> - <location line="+69"/> + <location line="+140"/> <source>Failed to enable encryption: %1</source> <translation>Не удалось включить шифрование: %1</translation> </message> <message> - <location line="+149"/> + <location line="+145"/> <source>Select an avatar</source> <translation>Выберите аватар</translation> </message> @@ -863,13 +1177,13 @@ Media size: %2 </message> <message> <location line="+12"/> - <source>The selected media is not an image</source> - <translation>Выбранное медия не является изображением</translation> + <source>The selected file is not an image</source> + <translation type="unfinished"></translation> </message> <message> <location line="+5"/> - <source>Error while reading media: %1</source> - <translation>Ошибка при чтении медия: %1</translation> + <source>Error while reading file: %1</source> + <translation type="unfinished"></translation> </message> <message> <location line="+35"/> @@ -886,7 +1200,7 @@ Media size: %2 <translation>Заблокировать пользователя в комнате</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Ignore messages from this user</source> <translation>Игнорировать сообщения от этого пользователя</translation> </message> @@ -896,19 +1210,161 @@ Media size: %2 <translation>Выгнать пользователя из комнаты</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Start a conversation</source> <translation>Начать разговор</translation> </message> <message> - <location line="+57"/> + <location line="+63"/> <source>Devices</source> <translation>Устройства</translation> </message> +</context> +<context> + <name>emoji::Panel</name> + <message> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> + <source>Smileys & People</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Animals & Nature</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Food & Drink</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Activity</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Travel & Places</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Objects</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Symbols</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>Flags</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> <message> - <location line="+39"/> - <source>ESC</source> - <translation></translation> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation type="unfinished"></translation> </message> </context> </TS> diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index 3ea4031c..536e39a2 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -2,37 +2,60 @@ <!DOCTYPE TS> <TS version="2.1" language="zh_CN"> <context> - <name>AudioItem</name> + <name>ChatPage</name> <message> - <location filename="../../src/timeline/widgets/AudioItem.cpp" line="+117"/> - <source>Save File</source> - <translation>保存文件</translation> + <location filename="../../src/ChatPage.cpp" line="+223"/> + <source>Failed to invite user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <location line="+868"/> + <source>Invited user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="-5"/> + <source>Failed to invite %1 to %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>Failed to kick %1 to %2: %3</source> + <translation type="unfinished"></translation> </message> -</context> -<context> - <name>ChatPage</name> <message> - <location filename="../../src/ChatPage.cpp" line="+304"/> - <source>Failed to upload image. Please try again.</source> - <translation>上传图像失败。请重试。</translation> + <location line="+5"/> + <source>Kicked user: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Failed to ban %1 in %2: %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Banned user: %1</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+45"/> - <source>Failed to upload file. Please try again.</source> - <translation>上传文件失败,请重试。</translation> + <location line="+14"/> + <source>Failed to unban %1 in %2: %3</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+43"/> - <source>Failed to upload audio. Please try again.</source> - <translation>上传音频失败。请重试。</translation> + <location line="+5"/> + <source>Unbanned user: %1</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+42"/> - <source>Failed to upload video. Please try again.</source> - <translation>上传视频失败。请重试。</translation> + <location line="-807"/> + <source>Failed to upload media. Please try again.</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+371"/> + <location line="+399"/> <source>Failed to restore OLM account. Please login again.</source> <translation>恢复 OLM 账户失败。请重新登录。</translation> </message> @@ -42,54 +65,90 @@ <translation>恢复保存的数据失败。请重新登录。</translation> </message> <message> - <location line="+167"/> - <source>Failed to setup encryption keys. Server response: %s %d. Please try again later.</source> - <translation>建立加密密钥失败。 服务器返回:%s %d. 请稍后重试。</translation> + <location line="+148"/> + <source>Failed to setup encryption keys. Server response: %1 %2. Please try again later.</source> + <translation type="unfinished"></translation> </message> <message> <location line="+51"/> - <location line="+150"/> + <location line="+231"/> <source>Please try to login again: %1</source> <translation>请尝试再次登录:%1</translation> </message> <message> - <location line="-45"/> + <location line="-154"/> + <source>Failed to join room: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You joined the room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Failed to remove invite: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> <source>Room creation failed: %1</source> <translation>创建聊天室失败:%1</translation> </message> <message> - <location line="+16"/> + <location line="+5"/> + <source>Room %1 created</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+11"/> <source>Failed to leave room: %1</source> <translation>离开聊天室失败:%1</translation> </message> </context> <context> - <name>DateSeparator</name> + <name>CommunitiesListItem</name> + <message> + <location filename="../../src/CommunitiesListItem.cpp" line="+133"/> + <source>All rooms</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Favourite rooms</source> + <translation type="unfinished"></translation> + </message> <message> - <location filename="../../src/ui/InfoMessage.cpp" line="+68"/> - <source>Today</source> - <translation>今天</translation> + <location line="+2"/> + <source>Low priority rooms</source> + <translation type="unfinished"></translation> </message> <message> <location line="+2"/> - <source>Yesterday</source> - <translation>昨天</translation> + <location line="+2"/> + <source> (tag)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source> (community)</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>EditModal</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+60"/> - <source>APPLY</source> - <translation>应用</translation> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+59"/> + <source>Apply</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+5"/> - <source>CANCEL</source> - <translation>取消</translation> + <location line="+1"/> + <source>Cancel</source> + <translation type="unfinished">取消</translation> </message> <message> - <location line="+12"/> + <location line="+10"/> <source>Name</source> <translation>名称</translation> </message> @@ -100,25 +159,25 @@ </message> </context> <context> - <name>FileItem</name> + <name>EncryptionIndicator</name> <message> - <location filename="../../src/timeline/widgets/FileItem.cpp" line="+106"/> - <source>Save File</source> - <translation>保存文件</translation> + <location filename="../qml/EncryptionIndicator.qml" line="+12"/> + <source>Encrypted</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>ImageItem</name> + <name>InviteeItem</name> <message> - <location filename="../../src/timeline/widgets/ImageItem.cpp" line="+238"/> - <source>Save image</source> - <translation>保存图像</translation> + <location filename="../../src/InviteeItem.cpp" line="+17"/> + <source>Remove</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>LoginPage</name> <message> - <location filename="../../src/LoginPage.cpp" line="+79"/> + <location filename="../../src/LoginPage.cpp" line="+82"/> <source>Matrix ID</source> <translation></translation> </message> @@ -143,7 +202,17 @@ <translation>登录</translation> </message> <message> - <location line="+85"/> + <location line="+84"/> + <source>Autodiscovery failed. Received malformed response.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Autodiscovery failed. Unknown error when requesting .well-known.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+24"/> <source>The required endpoints were not found. Possibly not a Matrix server.</source> <translation>没找到要求的终端。可能不是一个 Matrix 服务器。</translation> </message> @@ -166,20 +235,61 @@ <context> <name>MemberList</name> <message> - <location filename="../../src/dialogs/MemberList.cpp" line="+82"/> + <location filename="../../src/dialogs/MemberList.cpp" line="+90"/> <source>Room members</source> <translation>聊天室成员</translation> </message> <message> - <location line="+10"/> - <source>SHOW MORE</source> - <translation>显示更多</translation> + <location line="+4"/> + <source>OK</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>MessageDelegate</name> + <message> + <location filename="../qml/delegates/MessageDelegate.qml" line="+63"/> + <source>redacted</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>Encryption enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>room name changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed room name</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>topic changed to: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+0"/> + <source>removed topic</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>Placeholder</name> + <message> + <location filename="../qml/delegates/Placeholder.qml" line="+4"/> + <source>unimplemented event: </source> + <translation type="unfinished"></translation> </message> </context> <context> <name>QuickSwitcher</name> <message> - <location filename="../../src/QuickSwitcher.cpp" line="+71"/> + <location filename="../../src/QuickSwitcher.cpp" line="+74"/> <source>Search for a room...</source> <translation>寻找一个聊天室...</translation> </message> @@ -187,7 +297,7 @@ <context> <name>RegisterPage</name> <message> - <location filename="../../src/RegisterPage.cpp" line="+77"/> + <location filename="../../src/RegisterPage.cpp" line="+80"/> <source>Username</source> <translation>用户名</translation> </message> @@ -207,7 +317,7 @@ <translation>服务器</translation> </message> <message> - <location line="+17"/> + <location line="+16"/> <source>REGISTER</source> <translation>注册</translation> </message> @@ -233,19 +343,35 @@ </message> </context> <context> + <name>ReplyPopup</name> + <message> + <location filename="../../src/popups/ReplyPopup.cpp" line="+46"/> + <source>Logout</source> + <translation type="unfinished">登出</translation> + </message> +</context> +<context> + <name>RoomInfo</name> + <message> + <location filename="../../src/Cache.cpp" line="+2304"/> + <source>no version stored</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>RoomInfoListItem</name> <message> - <location filename="../../src/RoomInfoListItem.cpp" line="+79"/> + <location filename="../../src/RoomInfoListItem.cpp" line="+95"/> <source>Leave room</source> <translation>离开聊天室</translation> </message> <message> - <location line="+156"/> + <location line="+151"/> <source>Accept</source> <translation>接受</translation> </message> <message> - <location line="+1"/> + <location line="+3"/> <source>Decline</source> <translation>拒绝</translation> </message> @@ -253,7 +379,7 @@ <context> <name>SideBarActions</name> <message> - <location filename="../../src/SideBarActions.cpp" line="+32"/> + <location filename="../../src/SideBarActions.cpp" line="+38"/> <source>User settings</source> <translation>用户设置</translation> </message> @@ -281,41 +407,41 @@ <context> <name>StatusIndicator</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+169"/> - <source>Encrypted</source> - <translation>加密的</translation> + <location filename="../qml/StatusIndicator.qml" line="+14"/> + <source>Failed</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Delivered</source> - <translation>已送达</translation> + <location line="+1"/> + <source>Sent</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Seen</source> - <translation>已阅读</translation> + <location line="+1"/> + <source>Received</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+3"/> - <source>Sent</source> - <translation>已发送</translation> + <location line="+1"/> + <source>Read</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TextInputWidget</name> <message> - <location filename="../../src/TextInputWidget.cpp" line="+452"/> + <location filename="../../src/TextInputWidget.cpp" line="+493"/> <source>Send a file</source> <translation>发送一个文件</translation> </message> <message> - <location line="+17"/> - <location filename="../../src/TextInputWidget.h" line="+154"/> + <location line="+13"/> + <location filename="../../src/TextInputWidget.h" line="+159"/> <source>Write a message...</source> <translation>写一条消息...</translation> </message> <message> - <location line="+27"/> + <location line="+31"/> <source>Send a message</source> <translation>发送一条消息</translation> </message> @@ -325,7 +451,7 @@ <translation></translation> </message> <message> - <location line="+77"/> + <location line="+85"/> <source>Select a file</source> <translation>选择一个文件</translation> </message> @@ -341,30 +467,212 @@ </message> </context> <context> - <name>TimelineItem</name> + <name>TimelineModel</name> <message> - <location filename="../../src/timeline/TimelineItem.cpp" line="+78"/> + <location filename="../../src/timeline/TimelineModel.cpp" line="+716"/> + <source>-- Encrypted Event (No keys found for decryption) --</source> + <comment>Placeholder, when the message was not decrypted yet or can't be decrypted</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+15"/> + <source>-- Decryption Error (failed to communicate with DB) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed when trying to lookup the session.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+19"/> + <source>-- Decryption Error (failed to retrieve megolm keys from db) --</source> + <comment>Placeholder, when the message can't be decrypted, because the DB access failed.</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+12"/> + <source>-- Decryption Error (%1) --</source> + <comment>Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed ad %1</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+29"/> + <source>-- Encrypted Event (Unknown event type) --</source> + <comment>Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+47"/> <source>Message redaction failed: %1</source> - <translation>删除消息失败:%1</translation> + <translation type="unfinished">删除消息失败:%1</translation> + </message> + <message> + <location line="+454"/> + <source>Save image</source> + <translation type="unfinished">保存图像</translation> + </message> + <message> + <location line="+2"/> + <source>Save video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Save file</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location line="+126"/> + <source>%1 and %2 are typing</source> + <comment>Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.)</comment> + <translation type="unfinished"> + <numerusform></numerusform> + </translation> + </message> + <message> + <location line="+95"/> + <source>%1 was invited.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+9"/> + <source>%1 changed their display name and avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their display name.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 changed their avatar.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>%1 joined.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>%1 rejected their invite.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Revoked the invite to %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 left the room.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Kicked %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Unbanned %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 redacted their knock.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Rejected the knock from %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 left after having already left!</source> + <comment>This is a leave event after the user already left and shouln't happen apart from state resets</comment> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+6"/> + <source>%1 was banned.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>%1 knocked.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TimelineRow</name> + <message> + <location filename="../qml/TimelineRow.qml" line="+64"/> + <source>Reply</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>Options</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TimelineView</name> <message> - <location filename="../../src/timeline/TimelineView.cpp" line="+245"/> - <source>Encryption is enabled</source> - <translation>加密已启用</translation> + <location filename="../qml/TimelineView.qml" line="+32"/> + <source>Read receipts</source> + <translation type="unfinished">阅读回执</translation> + </message> + <message> + <location line="+4"/> + <source>Mark as read</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>View raw message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+4"/> + <source>Redact message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>Save as</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+14"/> + <source>No room open</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+239"/> + <source>Close</source> + <translation type="unfinished"></translation> </message> </context> <context> <name>TopRoomBar</name> <message> - <location filename="../../src/TopRoomBar.cpp" line="+68"/> + <location filename="../../src/TopRoomBar.cpp" line="+86"/> <source>Room options</source> <translation>聊天室选项</translation> </message> <message> - <location line="+28"/> + <location line="+5"/> + <source>Mentions</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+34"/> <source>Invite users</source> <translation>邀请用户</translation> </message> @@ -387,7 +695,7 @@ <context> <name>TrayIcon</name> <message> - <location filename="../../src/TrayIcon.cpp" line="+116"/> + <location filename="../../src/TrayIcon.cpp" line="+122"/> <source>Show</source> <translation>显示</translation> </message> @@ -398,22 +706,9 @@ </message> </context> <context> - <name>TypingDisplay</name> - <message> - <location filename="../../src/TypingDisplay.cpp" line="+49"/> - <source> is typing</source> - <translation> 正在打字</translation> - </message> - <message> - <location line="+2"/> - <source> are typing</source> - <translation> 正在打字</translation> - </message> -</context> -<context> <name>UserInfoWidget</name> <message> - <location filename="../../src/UserInfoWidget.cpp" line="+78"/> + <location filename="../../src/UserInfoWidget.cpp" line="+88"/> <source>Logout</source> <translation>登出</translation> </message> @@ -421,62 +716,82 @@ <context> <name>UserSettingsPage</name> <message> - <location filename="../../src/UserSettingsPage.cpp" line="+143"/> + <location filename="../../src/UserSettingsPage.cpp" line="+189"/> <source>Minimize to tray</source> <translation>最小化至托盘</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Start in tray</source> <translation>在托盘启动</translation> </message> <message> - <location line="+12"/> - <source>Re-order rooms based on activity</source> - <translation>根据活动重排序聊天室</translation> - </message> - <message> - <location line="+9"/> + <location line="+5"/> <source>Group's sidebar</source> <translation>群组侧边栏</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Circular Avatars</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Typing notifications</source> <translation>打字通知</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> <source>Read receipts</source> <translation>阅读回执</translation> </message> <message> - <location line="+9"/> + <location line="+3"/> + <source>Send messages as Markdown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> <source>Desktop notifications</source> <translation>桌面通知</translation> </message> <message> - <location line="+9"/> - <source>Scale factor (requires restart)</source> - <translation>缩放系数(需要重启)</translation> + <location line="+4"/> + <source>Scale factor</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+18"/> + <location line="+11"/> + <source>Font size</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+13"/> + <source>Font Family</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>Emoji Font Famly</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+33"/> <source>Theme</source> <translation>主题</translation> </message> <message> - <location line="+22"/> + <location line="+27"/> <source>Device ID</source> <translation>设备 ID</translation> </message> <message> - <location line="+11"/> + <location line="+12"/> <source>Device Fingerprint</source> <translation>设备指纹</translation> </message> <message> - <location line="+10"/> + <location line="+11"/> <source>Session Keys</source> <translation>会话密钥</translation> </message> @@ -501,48 +816,48 @@ <translation>通用</translation> </message> <message> - <location line="+184"/> + <location line="+168"/> <source>Open Sessions File</source> <translation>打开会话文件</translation> </message> <message> <location line="+4"/> - <location line="+16"/> + <location line="+18"/> <location line="+9"/> <location line="+2"/> <location line="+2"/> - <location line="+17"/> + <location line="+19"/> <location line="+10"/> - <location line="+13"/> + <location line="+18"/> <location line="+2"/> <location line="+2"/> <source>Error</source> <translation>错误</translation> </message> <message> - <location line="-64"/> - <location line="+30"/> + <location line="-73"/> + <location line="+32"/> <source>File Password</source> <translation>文件密码</translation> </message> <message> - <location line="-29"/> + <location line="-31"/> <source>Enter the passphrase to decrypt the file:</source> <translation>输入密码以解密文件:</translation> </message> <message> - <location line="+6"/> - <location line="+30"/> + <location line="+8"/> + <location line="+32"/> <source>The password cannot be empty</source> <translation>密码不能为空</translation> </message> <message> - <location line="-6"/> + <location line="-8"/> <source>Enter passphrase to encrypt your session keys:</source> <translation>输入密码以加密你的会话密钥:</translation> </message> <message> - <location line="+12"/> + <location line="+14"/> <source>File to save the exported session keys</source> <translation>保存导出的会话密钥的文件</translation> </message> @@ -550,7 +865,7 @@ <context> <name>WelcomePage</name> <message> - <location filename="../../src/WelcomePage.cpp" line="+44"/> + <location filename="../../src/WelcomePage.cpp" line="+47"/> <source>Welcome to nheko! The desktop client for the Matrix protocol.</source> <translation>欢迎使用 nheko! Matrix 协议的桌面客户端。</translation> </message> @@ -560,25 +875,38 @@ <translation>祝您使用愉快!</translation> </message> <message> - <location line="+19"/> + <location line="+23"/> <source>REGISTER</source> <translation>注册</translation> </message> <message> - <location line="+6"/> + <location line="+5"/> <source>LOGIN</source> <translation>登录</translation> </message> </context> <context> + <name>descriptiveTime</name> + <message> + <location filename="../../src/Utils.cpp" line="+138"/> + <source>Yesterday</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>dialogs::CreateRoom</name> <message> - <location filename="../../src/dialogs/CreateRoom.cpp" line="+41"/> - <source>CANCEL</source> - <translation>取消</translation> + <location filename="../../src/dialogs/CreateRoom.cpp" line="+36"/> + <source>Create room</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+11"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">取消</translation> + </message> + <message> + <location line="+10"/> <source>Name</source> <translation>名称</translation> </message> @@ -611,9 +939,9 @@ <context> <name>dialogs::InviteUsers</name> <message> - <location filename="../../src/dialogs/InviteUsers.cpp" line="+40"/> - <source>CANCEL</source> - <translation>取消</translation> + <location filename="../../src/dialogs/InviteUsers.cpp" line="+41"/> + <source>Cancel</source> + <translation type="unfinished">取消</translation> </message> <message> <location line="+8"/> @@ -624,12 +952,17 @@ <context> <name>dialogs::JoinRoom</name> <message> - <location filename="../../src/dialogs/JoinRoom.cpp" line="+34"/> - <source>CANCEL</source> - <translation>取消</translation> + <location filename="../../src/dialogs/JoinRoom.cpp" line="+30"/> + <source>Join</source> + <translation type="unfinished"></translation> </message> <message> - <location line="+8"/> + <location line="+2"/> + <source>Cancel</source> + <translation type="unfinished">取消</translation> + </message> + <message> + <location line="+7"/> <source>Room ID or alias</source> <translation>聊天室 ID 或别名</translation> </message> @@ -637,12 +970,12 @@ <context> <name>dialogs::LeaveRoom</name> <message> - <location filename="../../src/dialogs/LeaveRoom.cpp" line="+33"/> - <source>CANCEL</source> - <translation>取消</translation> + <location filename="../../src/dialogs/LeaveRoom.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">取消</translation> </message> <message> - <location line="+10"/> + <location line="+8"/> <source>Are you sure you want to leave?</source> <translation>你确定要离开吗?</translation> </message> @@ -650,12 +983,12 @@ <context> <name>dialogs::Logout</name> <message> - <location filename="../../src/dialogs/Logout.cpp" line="+52"/> - <source>CANCEL</source> - <translation>取消</translation> + <location filename="../../src/dialogs/Logout.cpp" line="+47"/> + <source>Cancel</source> + <translation type="unfinished">取消</translation> </message> <message> - <location line="+11"/> + <location line="+8"/> <source>Logout. Are you sure?</source> <translation>登出。确定吗?</translation> </message> @@ -663,7 +996,7 @@ <context> <name>dialogs::PreviewUploadOverlay</name> <message> - <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+42"/> + <location filename="../../src/dialogs/PreviewUploadOverlay.cpp" line="+41"/> <source>Upload</source> <translation>上传</translation> </message> @@ -673,7 +1006,7 @@ <translation>取消</translation> </message> <message> - <location line="+89"/> + <location line="+84"/> <source>Media type: %1 Media size: %2 </source> @@ -685,14 +1018,14 @@ Media size: %2 <context> <name>dialogs::ReCaptcha</name> <message> - <location filename="../../src/dialogs/ReCaptcha.cpp" line="+38"/> - <source>CONFIRM</source> - <translation>确定</translation> + <location filename="../../src/dialogs/ReCaptcha.cpp" line="+31"/> + <source>Cancel</source> + <translation type="unfinished">取消</translation> </message> <message> - <location line="+3"/> - <source>CANCEL</source> - <translation>取消</translation> + <location line="+1"/> + <source>Confirm</source> + <translation type="unfinished"></translation> </message> <message> <location line="+11"/> @@ -703,15 +1036,33 @@ Media size: %2 <context> <name>dialogs::ReadReceipts</name> <message> - <location filename="../../src/dialogs/ReadReceipts.cpp" line="+104"/> + <location filename="../../src/dialogs/ReadReceipts.cpp" line="+117"/> <source>Read receipts</source> <translation>阅读回执</translation> </message> + <message> + <location line="+4"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>dialogs::ReceiptItem</name> + <message> + <location line="-44"/> + <source>Today %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+2"/> + <source>Yesterday %1</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>dialogs::RoomSettings</name> <message> - <location filename="../../src/dialogs/RoomSettings.cpp" line="+109"/> + <location filename="../../src/dialogs/RoomSettings.cpp" line="+113"/> <source>Settings</source> <translation>设置</translation> </message> @@ -726,17 +1077,22 @@ Media size: %2 <translation>内部 ID</translation> </message> <message> + <location line="+10"/> + <source>Room Version</source> + <translation type="unfinished"></translation> + </message> + <message> <location line="+4"/> <source>Notifications</source> <translation>通知</translation> </message> <message> - <location line="+3"/> + <location line="+2"/> <source>Muted</source> <translation>静默</translation> </message> <message> - <location line="+1"/> + <location line="+2"/> <source>Mentions only</source> <translation>只限提及</translation> </message> @@ -746,7 +1102,7 @@ Media size: %2 <translation>所有消息</translation> </message> <message> - <location line="+7"/> + <location line="+97"/> <source>Room access</source> <translation>聊天室访问</translation> </message> @@ -795,19 +1151,19 @@ Media size: %2 这是一个临时的测试端到端加密的方案。</translation> </message> <message numerus="yes"> - <location line="+53"/> + <location line="+51"/> <source>%n member(s)</source> <translation> <numerusform>%n 成员</numerusform> </translation> </message> <message> - <location line="+126"/> + <location line="+140"/> <source>Failed to enable encryption: %1</source> <translation>启用加密失败:%1</translation> </message> <message> - <location line="+158"/> + <location line="+145"/> <source>Select an avatar</source> <translation>选择一个头像</translation> </message> @@ -818,13 +1174,13 @@ Media size: %2 </message> <message> <location line="+12"/> - <source>The selected media is not an image</source> - <translation>选择的媒体不是一个图像</translation> + <source>The selected file is not an image</source> + <translation type="unfinished"></translation> </message> <message> <location line="+5"/> - <source>Error while reading media: %1</source> - <translation>读取媒体时失败:%1</translation> + <source>Error while reading file: %1</source> + <translation type="unfinished"></translation> </message> <message> <location line="+35"/> @@ -836,12 +1192,12 @@ Media size: %2 <context> <name>dialogs::UserProfile</name> <message> - <location filename="../../src/dialogs/UserProfile.cpp" line="+59"/> + <location filename="../../src/dialogs/UserProfile.cpp" line="+63"/> <source>Ban the user from the room</source> <translation>在这个聊天室封禁这个用户</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Ignore messages from this user</source> <translation>忽略这个用户的消息</translation> </message> @@ -851,12 +1207,12 @@ Media size: %2 <translation>把这个用户踢出聊天室</translation> </message> <message> - <location line="+9"/> + <location line="+8"/> <source>Start a conversation</source> <translation>开始一个聊天</translation> </message> <message> - <location line="+56"/> + <location line="+63"/> <source>Devices</source> <translation>设备</translation> </message> @@ -864,7 +1220,7 @@ Media size: %2 <context> <name>emoji::Panel</name> <message> - <location filename="../../src/emoji/Panel.cpp" line="+125"/> + <location filename="../../src/emoji/Panel.cpp" line="+126"/> <source>Smileys & People</source> <translatorcomment>笑脸和人</translatorcomment> <translation>Smileys & People</translation> @@ -912,4 +1268,108 @@ Media size: %2 <translation>Flags</translation> </message> </context> +<context> + <name>message-description sent:</name> + <message> + <location filename="../../src/Utils.h" line="+100"/> + <source>You sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an audio clip</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an image</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a video</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a sticker</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent a notification</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+5"/> + <source>You: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+7"/> + <source>You sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+3"/> + <source>%1 sent an encrypted message</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>popups::UserMentions</name> + <message> + <location filename="../../src/popups/UserMentions.cpp" line="+64"/> + <source>This Room</source> + <translation type="unfinished"></translation> + </message> + <message> + <location line="+1"/> + <source>All Rooms</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>utils</name> + <message> + <location filename="../../src/Utils.h" line="+4"/> + <source>Unknown Message Type</source> + <translation type="unfinished"></translation> + </message> +</context> </TS> diff --git a/resources/login.png b/resources/login.png index e65084ef..d098a62a 100644 --- a/resources/login.png +++ b/resources/login.png Binary files differdiff --git a/resources/login@2x.png b/resources/login@2x.png index 4f89112f..9cbe3c9f 100644 --- a/resources/login@2x.png +++ b/resources/login@2x.png Binary files differdiff --git a/resources/nheko-1024.png b/resources/nheko-1024.png index 45b19a62..8a59d5f6 100644 --- a/resources/nheko-1024.png +++ b/resources/nheko-1024.png Binary files differdiff --git a/resources/nheko-128.png b/resources/nheko-128.png index e65084ef..d098a62a 100644 --- a/resources/nheko-128.png +++ b/resources/nheko-128.png Binary files differdiff --git a/resources/nheko-16.png b/resources/nheko-16.png index deb4449d..7114e060 100644 --- a/resources/nheko-16.png +++ b/resources/nheko-16.png Binary files differdiff --git a/resources/nheko-256.png b/resources/nheko-256.png index 4f89112f..9cbe3c9f 100644 --- a/resources/nheko-256.png +++ b/resources/nheko-256.png Binary files differdiff --git a/resources/nheko-32.png b/resources/nheko-32.png index ec582489..5fefc6b5 100644 --- a/resources/nheko-32.png +++ b/resources/nheko-32.png Binary files differdiff --git a/resources/nheko-48.png b/resources/nheko-48.png index e5aab6ac..726d0356 100644 --- a/resources/nheko-48.png +++ b/resources/nheko-48.png Binary files differdiff --git a/resources/nheko-512.png b/resources/nheko-512.png index 3c39b0be..29c3a607 100644 --- a/resources/nheko-512.png +++ b/resources/nheko-512.png Binary files differdiff --git a/resources/nheko-64.png b/resources/nheko-64.png index 768921c9..1b5c9eb3 100644 --- a/resources/nheko-64.png +++ b/resources/nheko-64.png Binary files differdiff --git a/resources/nheko.png b/resources/nheko.png index 3c39b0be..ae7398a6 100644 --- a/resources/nheko.png +++ b/resources/nheko.png Binary files differdiff --git a/resources/nheko.svg b/resources/nheko.svg new file mode 100644 index 00000000..ce3ec406 --- /dev/null +++ b/resources/nheko.svg @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="1024" + height="1024" + viewBox="0 0 270.93333 270.93333" + version="1.1" + id="svg8" + inkscape:version="0.92.4 5da689c313, 2019-01-14" + sodipodi:docname="nheko.svg" + inkscape:export-filename="/home/nicolas/Dokumente/devel/open-source/nheko/resources/nheko-rebuild-round-corners.svg.png" + inkscape:export-xdpi="130.048" + inkscape:export-ydpi="130.048"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.35355339" + inkscape:cx="852.07808" + inkscape:cy="-60.410565" + inkscape:document-units="mm" + inkscape:current-layer="layer2" + showgrid="true" + inkscape:window-width="1920" + inkscape:window-height="1019" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + showguides="true" + inkscape:snap-grids="true" + gridtolerance="10" + inkscape:snap-bbox="false" + inkscape:bbox-paths="true" + inkscape:snap-global="true" + inkscape:bbox-nodes="true" + inkscape:lockguides="false" + units="px"> + <sodipodi:guide + position="0,0" + orientation="0,793.70079" + id="guide4797" + inkscape:locked="false" /> + <sodipodi:guide + position="0,297" + orientation="1122.5197,0" + id="guide4803" + inkscape:locked="false" /> + <inkscape:grid + type="axonomgrid" + id="grid4805" + units="px" + empspacing="2" + snapvisiblegridlinesonly="true" + spacingy="1.0583333" /> + <sodipodi:guide + position="0,0" + orientation="0,755.90551" + id="guide4807" + inkscape:locked="false" /> + <sodipodi:guide + position="200,0" + orientation="-755.90551,0" + id="guide4809" + inkscape:locked="false" /> + <sodipodi:guide + position="200,200" + orientation="0,-755.90551" + id="guide4811" + inkscape:locked="false" /> + <inkscape:grid + type="xygrid" + id="grid871" + empspacing="2" + color="#d43fff" + opacity="0.1254902" + empcolor="#cf3fff" + empopacity="0.25098039" + units="px" + spacingx="1.0583333" + spacingy="1.0583333" + enabled="false" /> + </sodipodi:namedview> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Logo" + style="display:inline" + transform="translate(0,-26.066668)"> + <circle + id="path3792" + cx="135.46666" + cy="161.53333" + style="display:inline;fill:#333333;fill-opacity:1;stroke:none;stroke-width:0.3584221" + inkscape:transform-center-x="-57.929751" + inkscape:transform-center-y="532.03976" + inkscape:export-xdpi="96.000008" + inkscape:export-ydpi="96.000008" + r="135.46666" /> + <path + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.32663074px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 48.965212,110.73276 H 239.52342 c 4.88824,0 4.88824,0 0,8.46688 L 180.59519,221.2662 c -4.6188,8.00001 -4.6188,8.00001 -9.50702,8.00001 h -19.55294 c -4.88824,0 -4.88824,0 -0.26944,-8.00001 l 44.2635,-76.66608 h -29.41224 l -43.91123,76.19952 c -4.88823,8.46657 -4.88823,8.46657 -9.77646,8.46657 H 29.329398 l 49.299816,-84.66609 h -49.29982 z" + id="path4834" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccc" + inkscape:export-xdpi="96.000008" + inkscape:export-ydpi="96.000008" /> + <path + style="fill:#c0def5;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 97.764652,110.73276 H 127.09406 L 58.658797,229.26621 H 29.329398 Z" + id="path4836" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" + inkscape:export-xdpi="96.000008" + inkscape:export-ydpi="96.000008" /> + <path + style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 58.658797,229.26621 127.09406,110.73276 h 29.3294 L 87.988193,229.26621 Z" + id="path4838" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" + inkscape:export-xdpi="96.000008" + inkscape:export-ydpi="96.000008" /> + </g> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + style="display:inline" + transform="translate(0,-26.066668)" /> +</svg> diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml new file mode 100644 index 00000000..0a53eac9 --- /dev/null +++ b/resources/qml/Avatar.qml @@ -0,0 +1,53 @@ +import QtQuick 2.6 +import QtGraphicalEffects 1.0 +import Qt.labs.settings 1.0 + +Rectangle { + id: avatar + width: 48 + height: 48 + radius: settings.avatar_circles ? height/2 : 3 + + Settings { + id: settings + category: "user" + property bool avatar_circles: true + } + + property alias url: img.source + property string displayName + + Text { + anchors.fill: parent + text: chat.model.escapeEmoji(String.fromCodePoint(displayName.codePointAt(0))) + textFormat: Text.RichText + color: colors.text + font.pixelSize: avatar.height/2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + visible: img.status != Image.Ready + } + + Image { + id: img + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: false + + sourceSize.width: avatar.width + sourceSize.height: avatar.height + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + anchors.fill: parent + width: avatar.width + height: avatar.height + radius: settings.avatar_circles ? height/2 : 3 + } + } + } + color: colors.base +} diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml new file mode 100644 index 00000000..00fe2ee4 --- /dev/null +++ b/resources/qml/EncryptionIndicator.qml @@ -0,0 +1,26 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import im.nheko 1.0 + +Rectangle { + id: indicator + color: "transparent" + width: 16 + height: 16 + + ToolTip.visible: ma.containsMouse && indicator.visible + ToolTip.text: qsTr("Encrypted") + + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + anchors.fill: parent + source: "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText + } +} + diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml new file mode 100644 index 00000000..dc576e18 --- /dev/null +++ b/resources/qml/ImageButton.qml @@ -0,0 +1,29 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 + +Button { + property string image: undefined + + id: button + + flat: true + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: buttonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText) + } + + MouseArea + { + id: mouseArea + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: Qt.PointingHandCursor + } +} diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml new file mode 100644 index 00000000..9a4f7348 --- /dev/null +++ b/resources/qml/MatrixText.qml @@ -0,0 +1,32 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.3 + +TextEdit { + textFormat: TextEdit.RichText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + color: colors.text + + onLinkActivated: { + if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) + else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) + else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { + var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) + timelineManager.setHistoryView(match[1]) + chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) + } + else Qt.openUrlExternally(link) + } + MouseArea + { + id: ma + anchors.fill: parent + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + ToolTip.visible: hoveredLink + ToolTip.text: hoveredLink +} diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml new file mode 100644 index 00000000..ec82ed49 --- /dev/null +++ b/resources/qml/StatusIndicator.qml @@ -0,0 +1,39 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import im.nheko 1.0 + +Rectangle { + id: indicator + property int state: 0 + color: "transparent" + width: 16 + height: 16 + + ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty + ToolTip.text: switch (state) { + case MtxEvent.Failed: return qsTr("Failed") + case MtxEvent.Sent: return qsTr("Sent") + case MtxEvent.Received: return qsTr("Received") + case MtxEvent.Read: return qsTr("Read") + default: return "" + } + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: switch (indicator.state) { + case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText + case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText + case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText + case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText + default: return "" + } + } +} + diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml new file mode 100644 index 00000000..2984844f --- /dev/null +++ b/resources/qml/TimelineRow.qml @@ -0,0 +1,134 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 + +import im.nheko 1.0 + +import "./delegates" + +MouseArea { + id: rowArea + + anchors.left: parent.left + anchors.right: parent.right + height: row.height + + hoverEnabled: true + preventStealing: true + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + + property bool showButtons: false + + Timer { + running: rowArea.containsMouse + interval: 150 + onTriggered: rowArea.state = "showButtons" + } + + states: [ + State { + name: "hideButtons" + when: !rowArea.containsMouse + PropertyChanges { target: rowArea; showButtons: false; } + }, + State { + name: "showButtons" + PropertyChanges { target: rowArea; showButtons: true; } + } + ] + + RowLayout { + id: row + + anchors.leftMargin: avatarSize + 4 + anchors.left: parent.left + anchors.right: parent.right + + + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 4 + + // fancy reply, if this is a reply + Reply { + visible: model.replyTo + modelData: chat.model.getDump(model.replyTo) + userColor: timelineManager.userColor(modelData.userId, colors.window) + } + + // actual message content + MessageDelegate { + id: contentItem + + width: parent.width + + modelData: model + } + } + + ImageButton { + visible: rowArea.showButtons + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + id: replyButton + hoverEnabled: true + + + image: ":/icons/icons/ui/mail-reply.png" + + ToolTip.visible: hovered + ToolTip.text: qsTr("Reply") + + onClicked: chat.model.replyAction(model.id) + } + ImageButton { + visible: rowArea.showButtons + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + id: optionsButton + hoverEnabled: true + + image: ":/icons/icons/ui/vertical-ellipsis.png" + + ToolTip.visible: hovered + ToolTip.text: qsTr("Options") + + onClicked: messageContextMenu.show(model.id, model.type, optionsButton) + + } + + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + } + + EncryptionIndicator { + visible: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + } + + Text { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: model.timestamp.toLocaleTimeString("HH:mm") + color: inactiveColors.text + + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + ToolTip.visible: ma.containsMouse + ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + } + } +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml new file mode 100644 index 00000000..46cf484b --- /dev/null +++ b/resources/qml/TimelineView.qml @@ -0,0 +1,309 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtQuick.Window 2.2 + +import im.nheko 1.0 + +import "./delegates" + +Item { + property var colors: currentActivePalette + property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } + property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive + property int avatarSize: 40 + + Menu { + id: messageContextMenu + palette: colors + modal: true + + function show(eventId_, eventType_, showAt) { + eventId = eventId_ + eventType = eventType_ + popup(showAt) + } + + property string eventId + property int eventType + + MenuItem { + text: qsTr("Read receipts") + onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) + } + MenuItem { + text: qsTr("Mark as read") + } + MenuItem { + text: qsTr("View raw message") + onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) + } + MenuItem { + text: qsTr("Redact message") + onTriggered: chat.model.redactEvent(messageContextMenu.eventId) + } + MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + text: qsTr("Save as") + onTriggered: timelineManager.timeline.saveMedia(messageContextMenu.eventId) + } + } + + id: timelineRoot + + Rectangle { + anchors.fill: parent + color: colors.window + + Text { + visible: !timelineManager.timeline && !timelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: colors.windowText + } + + BusyIndicator { + anchors.centerIn: parent + running: timelineManager.isInitialSync + height: 200 + width: 200 + z: 3 + } + + ListView { + id: chat + + visible: timelineManager.timeline != null + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: chatFooter.top + + anchors.leftMargin: 4 + anchors.rightMargin: scrollbar.width + + model: timelineManager.timeline + + boundsBehavior: Flickable.StopAtBounds + pixelAligned: true + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + onWheel: { + if (wheel.angleDelta != 0) { + chat.contentY = chat.contentY - wheel.angleDelta.y + wheel.accepted = true + chat.returnToBounds() + } + } + } + + Shortcut { + sequence: StandardKey.MoveToPreviousPage + onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); } + } + Shortcut { + sequence: StandardKey.MoveToNextPage + onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); } + } + + ScrollBar.vertical: ScrollBar { + id: scrollbar + parent: chat.parent + anchors.top: chat.top + anchors.left: chat.right + anchors.bottom: chat.bottom + } + + spacing: 4 + verticalLayoutDirection: ListView.BottomToTop + + onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom + + delegate: Rectangle { + // This would normally be previousSection, but our model's order is inverted. + property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 + + id: wrapper + property Item section + width: chat.width + height: section ? section.height + timelinerow.height : timelinerow.height + color: "transparent" + + TimelineRow { + id: timelinerow + y: section ? section.y + section.height : 0 + } + + onSectionBoundaryChanged: { + if (sectionBoundary) { + var properties = { + 'modelData': model.dump, + 'section': ListView.section, + 'nextSection': ListView.nextSection + } + section = sectionHeader.createObject(wrapper, properties) + } else { + section.destroy() + section = null + } + } + + Binding { + target: chat.model + property: "currentIndex" + when: y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height + value: index + delayed: true + } + + } + + section { + property: "section" + } + Component { + id: sectionHeader + Column { + property var modelData + property string section + property string nextSection + + topPadding: 4 + bottomPadding: 4 + spacing: 8 + + visible: !!modelData + + width: parent.width + height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 + + Label { + id: dateBubble + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + visible: section.includes(" ") + text: chat.model.formatDateSeparator(modelData.timestamp) + color: colors.windowText + + height: contentHeight * 1.2 + width: contentWidth * 1.2 + horizontalAlignment: Text.AlignHCenter + background: Rectangle { + radius: parent.height / 2 + color: colors.base + } + } + Row { + height: userName.height + spacing: 4 + Avatar { + width: avatarSize + height: avatarSize + url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") + displayName: modelData.userName + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(modelData.userId) + cursorShape: Qt.PointingHandCursor + } + } + + Text { + id: userName + text: chat.model.escapeEmoji(modelData.userName) + color: timelineManager.userColor(modelData.userId, colors.window) + textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(section.split(" ")[0]) + cursorShape: Qt.PointingHandCursor + } + } + } + } + } + + } + + Rectangle { + id: chatFooter + + height: Math.max(16, footerContent.height) + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + z: 3 + + color: "transparent" + + Column { + id: footerContent + anchors.left: parent.left + anchors.right: parent.right + + Text { + id: typingDisplay + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 10 + anchors.rightMargin: 10 + + text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" + textFormat: Text.RichText + color: colors.windowText + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + + id: replyPopup + + visible: timelineManager.replyingEvent && chat.model + // Height of child, plus margins, plus border + height: replyPreview.height + 10 + color: colors.base + + + Reply { + id: replyPreview + + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: closeReplyButton.left + anchors.rightMargin: 20 + anchors.bottom: parent.bottom + + modelData: chat.model ? chat.model.getDump(timelineManager.replyingEvent) : {} + userColor: timelineManager.userColor(modelData.userId, colors.window) + } + + ImageButton { + id: closeReplyButton + + anchors.right: parent.right + anchors.rightMargin: 15 + anchors.top: replyPreview.top + hoverEnabled: true + width: 16 + height: 16 + + image: ":/icons/icons/ui/remove-symbol.png" + ToolTip.visible: closeReplyButton.hovered + ToolTip.text: qsTr("Close") + + onClicked: timelineManager.closeReply() + } + } + } + } + } +} diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml new file mode 100644 index 00000000..2fe0a490 --- /dev/null +++ b/resources/qml/delegates/FileMessage.qml @@ -0,0 +1,57 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.2 + +Rectangle { + radius: 10 + color: colors.base + height: row.height + 24 + width: parent ? parent.width : undefined + + RowLayout { + id: row + + anchors.centerIn: parent + width: parent.width - 24 + + spacing: 15 + + Rectangle { + id: button + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: timelineManager.timeline.saveMedia(model.data.id) + cursorShape: Qt.PointingHandCursor + } + } + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: model.data.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + Text { + Layout.fillWidth: true + text: model.data.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + } + } +} diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml new file mode 100644 index 00000000..cb05021d --- /dev/null +++ b/resources/qml/delegates/ImageMessage.qml @@ -0,0 +1,28 @@ +import QtQuick 2.6 + +import im.nheko 1.0 + +Item { + property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width) + property double tempHeight: tempWidth * model.data.proportionalHeight + + property bool tooHigh: tempHeight > timelineRoot.height / 2 + + height: tooHigh ? timelineRoot.height / 2 : tempHeight + width: tooHigh ? (timelineRoot.height / 2) / model.data.proportionalHeight : tempWidth + + Image { + id: img + anchors.fill: parent + + source: model.data.url.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit + + MouseArea { + enabled: model.data.type == MtxEvent.ImageMessage + anchors.fill: parent + onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id) + } + } +} diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml new file mode 100644 index 00000000..ab633087 --- /dev/null +++ b/resources/qml/delegates/MessageDelegate.qml @@ -0,0 +1,94 @@ +import QtQuick 2.6 +import im.nheko 1.0 + +Item { + // Workaround to have an assignable global property + Item { + id: model + property var data; + } + + property alias modelData: model.data + + height: chooser.childrenRect.height + + DelegateChooser { + id: chooser + //role: "type" //< not supported in our custom implementation, have to use roleValue + roleValue: model.data.type + anchors.fill: parent + + DelegateChoice { + roleValue: MtxEvent.UnknownMessage + Placeholder { text: "Unretrieved event" } + } + DelegateChoice { + roleValue: MtxEvent.TextMessage + TextMessage {} + } + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + NoticeMessage {} + } + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + NoticeMessage { + formatted: chat.model.escapeEmoji(modelData.userName) + " " + model.data.formattedBody + color: timelineManager.userColor(modelData.userId, colors.window) + } + } + DelegateChoice { + roleValue: MtxEvent.ImageMessage + ImageMessage {} + } + DelegateChoice { + roleValue: MtxEvent.Sticker + ImageMessage {} + } + DelegateChoice { + roleValue: MtxEvent.FileMessage + FileMessage {} + } + DelegateChoice { + roleValue: MtxEvent.VideoMessage + PlayableMediaMessage {} + } + DelegateChoice { + roleValue: MtxEvent.AudioMessage + PlayableMediaMessage {} + } + DelegateChoice { + roleValue: MtxEvent.Redacted + Pill { + text: qsTr("redacted") + } + } + DelegateChoice { + roleValue: MtxEvent.Encryption + Pill { + text: qsTr("Encryption enabled") + } + } + DelegateChoice { + roleValue: MtxEvent.Name + NoticeMessage { + text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") + } + } + DelegateChoice { + roleValue: MtxEvent.Topic + NoticeMessage { + text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") + } + } + DelegateChoice { + roleValue: MtxEvent.Member + NoticeMessage { + text: timelineManager.timeline.formatMemberEvent(model.data.id); + } + } + DelegateChoice { + Placeholder {} + } + } +} diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml new file mode 100644 index 00000000..12664fb5 --- /dev/null +++ b/resources/qml/delegates/NoticeMessage.qml @@ -0,0 +1,4 @@ +TextMessage { + font.italic: true + color: inactiveColors.text +} diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml new file mode 100644 index 00000000..b19d9a54 --- /dev/null +++ b/resources/qml/delegates/Pill.qml @@ -0,0 +1,14 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 + +Label { + color: inactiveColors.text + horizontalAlignment: Text.AlignHCenter + + height: contentHeight * 1.2 + width: contentWidth * 1.2 + background: Rectangle { + radius: parent.height / 2 + color: colors.base + } +} diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml new file mode 100644 index 00000000..26de2067 --- /dev/null +++ b/resources/qml/delegates/Placeholder.qml @@ -0,0 +1,7 @@ +import ".." + +MatrixText { + text: qsTr("unimplemented event: ") + model.data.typeString + width: parent ? parent.width : undefined + color: inactiveColors.text +} diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml new file mode 100644 index 00000000..a4096864 --- /dev/null +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -0,0 +1,167 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.1 +import QtMultimedia 5.6 + +import im.nheko 1.0 + +Rectangle { + id: bg + radius: 10 + color: colors.base + height: content.height + 24 + width: parent ? parent.width : undefined + + Column { + id: content + width: parent.width - 24 + anchors.centerIn: parent + + Rectangle { + id: videoContainer + visible: model.data.type == MtxEvent.VideoMessage + width: Math.min(parent.width, model.data.width ? model.data.width : 400) // some media has 0 as size... + height: width*model.data.proportionalHeight + Image { + anchors.fill: parent + source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit + + VideoOutput { + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + source: media + } + } + } + + RowLayout { + width: parent.width + Text { + id: positionText + text: "--:--:--" + color: colors.text + } + Slider { + Layout.fillWidth: true + id: progress + value: media.position + from: 0 + to: media.duration + + onMoved: media.seek(value) + //indeterminate: true + function updatePositionTexts() { + function formatTime(date) { + var hh = date.getUTCHours(); + var mm = date.getUTCMinutes(); + var ss = date.getSeconds(); + if (hh < 10) {hh = "0"+hh;} + if (mm < 10) {mm = "0"+mm;} + if (ss < 10) {ss = "0"+ss;} + return hh+":"+mm+":"+ss; + } + positionText.text = formatTime(new Date(media.position)) + durationText.text = formatTime(new Date(media.duration)) + } + onValueChanged: updatePositionTexts() + + palette: colors + } + Text { + id: durationText + text: "--:--:--" + color: colors.text + } + } + + RowLayout { + width: parent.width + + spacing: 15 + + Rectangle { + id: button + color: colors.window + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + z: 3 + + source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.text + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: { + switch (button.state) { + case "": timelineManager.timeline.cacheMedia(model.data.id); break; + case "stopped": + media.play(); console.log("play"); + button.state = "playing" + break + case "playing": + media.pause(); console.log("pause"); + button.state = "stopped" + break + } + } + cursorShape: Qt.PointingHandCursor + } + MediaPlayer { + id: media + onError: console.log(errorString) + onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() + onStopped: button.state = "stopped" + } + + Connections { + target: timelineManager.timeline + onMediaCached: { + if (mxcUrl == model.data.url) { + media.source = "file://" + cacheUrl + button.state = "stopped" + console.log("media loaded: " + mxcUrl + " at " + cacheUrl) + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl) + } + } + + states: [ + State { + name: "stopped" + PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.text } + }, + State { + name: "playing" + PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.text } + } + ] + } + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: model.data.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + Text { + Layout.fillWidth: true + text: model.data.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + } + } + } +} + diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml new file mode 100644 index 00000000..06804328 --- /dev/null +++ b/resources/qml/delegates/Reply.qml @@ -0,0 +1,58 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 + +Rectangle { + id: replyComponent + + property alias modelData: reply.modelData + property color userColor: "red" + + width: parent.width + height: replyContainer.height + + MouseArea { + anchors.fill: parent + preventStealing: true + onClicked: chat.positionViewAtIndex(chat.model.idToIndex(timelineManager.replyingEvent), ListView.Contain) + cursorShape: Qt.PointingHandCursor + } + + Rectangle { + id: colorLine + + anchors.top: replyContainer.top + anchors.bottom: replyContainer.bottom + width: 4 + + color: timelineManager.userColor(reply.modelData.userId, colors.window) + } + + Column { + id: replyContainer + anchors.left: colorLine.right + anchors.leftMargin: 4 + width: parent.width - 8 + + Text { + id: userName + text: chat.model ? chat.model.escapeEmoji(reply.modelData.userName) : "" + color: replyComponent.userColor + textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(reply.modelData.userId) + cursorShape: Qt.PointingHandCursor + } + } + + MessageDelegate { + id: reply + width: parent.width + } + } + + color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2) +} diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml new file mode 100644 index 00000000..7e4b1f29 --- /dev/null +++ b/resources/qml/delegates/TextMessage.qml @@ -0,0 +1,7 @@ +import ".." + +MatrixText { + property string formatted: model.data.formattedBody + text: "<style type=\"text/css\">a { color:"+colors.link+";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>") + width: parent ? parent.width : undefined +} diff --git a/resources/register.png b/resources/register.png index e65084ef..d098a62a 100644 --- a/resources/register.png +++ b/resources/register.png Binary files differdiff --git a/resources/register@2x.png b/resources/register@2x.png index 4f89112f..9cbe3c9f 100644 --- a/resources/register@2x.png +++ b/resources/register@2x.png Binary files differdiff --git a/resources/res.qrc b/resources/res.qrc index cef55773..7080fdd6 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -1,5 +1,6 @@ <RCC> <qresource prefix="/icons"> + <file>icons/ui/at-solid.svg</file> <file>icons/ui/volume-off-indicator.png</file> <file>icons/ui/volume-off-indicator@2x.png</file> <file>icons/ui/black-bubble-speech.png</file> @@ -62,6 +63,8 @@ <file>icons/ui/edit.png</file> <file>icons/ui/edit@2x.png</file> + + <file>icons/ui/mail-reply.png</file> <file>icons/emoji-categories/people.png</file> <file>icons/emoji-categories/people@2x.png</file> @@ -82,6 +85,7 @@ </qresource> <qresource prefix="/logos"> <file>nheko.png</file> + <file>nheko.svg</file> <file>splash.png</file> <file>splash@2x.png</file> @@ -99,16 +103,27 @@ <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> <file>styles/nheko-dark.qss</file> </qresource> + <qresource prefix="/"> + <file>qml/TimelineView.qml</file> + <file>qml/Avatar.qml</file> + <file>qml/ImageButton.qml</file> + <file>qml/MatrixText.qml</file> + <file>qml/StatusIndicator.qml</file> + <file>qml/EncryptionIndicator.qml</file> + <file>qml/TimelineRow.qml</file> + <file>qml/delegates/MessageDelegate.qml</file> + <file>qml/delegates/TextMessage.qml</file> + <file>qml/delegates/NoticeMessage.qml</file> + <file>qml/delegates/ImageMessage.qml</file> + <file>qml/delegates/PlayableMediaMessage.qml</file> + <file>qml/delegates/FileMessage.qml</file> + <file>qml/delegates/Pill.qml</file> + <file>qml/delegates/Placeholder.qml</file> + <file>qml/delegates/Reply.qml</file> + </qresource> </RCC> diff --git a/resources/splash.png b/resources/splash.png index 4f89112f..9cbe3c9f 100644 --- a/resources/splash.png +++ b/resources/splash.png Binary files differdiff --git a/resources/splash@2x.png b/resources/splash@2x.png index 3c39b0be..29c3a607 100644 --- a/resources/splash@2x.png +++ b/resources/splash@2x.png Binary files differdiff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index 5567f32c..4efb6f30 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -3,33 +3,86 @@ QLabel { color: #caccd1; } -TimelineItem { - qproperty-backgroundColor: #202228; +TextLabel::a { + color: #38a3d8; } -#chatPage, -#chatPage > * { +QuickSwitcher, +ReplyPopup, +SuggestionsPopup, +UserSettingsPage, +#scroll_widget, +#UserSettingScrollWidget { background-color: #202228; } -#sideBar { +#chatPage, +#chatPage > *, +CommunitiesList, +CommunitiesList > *, +RoomList, +RoomList > *, +TimelineView, +TimelineView > *, +UserMentionsWidget, +UserMentionsWidget > * { + background-color: #2d3139; border: none; - border-right: 1px solid #202228; - border-left: 1px solid #202228; } -TimelineView, -TimelineView > * { +QLineEdit, +QListWidget, +WelcomePage, +LoginPage, +RegisterPage, +EditModal, +emoji--Panel, +emoji--Panel > *, +dialogs--Logout, +dialogs--ReCaptcha, +dialogs--LeaveRoom, +dialogs--CreateRoom, +dialogs--RoomSettings, +dialogs--InviteUsers, +dialogs--ReadReceipts, +dialogs--JoinRoom, +dialogs--MemberList, +dialogs--PreviewUploadOverlay, +dialogs--UserProfile, +dialogs--CreateRoom > QLineEdit, +dialogs--InviteUsers > QLineEdit, +dialogs--JoinRoom > QLineEdit { background-color: #202228; - border: none; + color: #caccd1; } -#scroll_widget { - background-color: #202228; +emoji--Panel QWidget { border: none; } +emoji--Panel QScrollBar:vertical { width: 0px; margin: 0px; } +emoji--Panel QScrollBar::handle:vertical { min-height: 30px; } + +emoji--Category, +emoji--Category > * { + background-color: #2d3139; + color: #727274; } -QuickSwitcher { - background-color: #202228; +emoji--Category QLabel { + margin: 20px 0 20px 8px; +} + +TimelineItem { + qproperty-backgroundColor: #202228; +} + +#sideBar { + border: none; + border-right: 1px solid #202228; + border-left: 1px solid #202228; +} + +UserMentionsWidget > TimelineItem { + qproperty-backgroundColor: #202228; + qproperty-hoverColor: rgba(45, 49, 57, 120); } InfoMessage { @@ -37,21 +90,11 @@ InfoMessage { qproperty-boxColor: rgba(45, 49, 57, 120); } -SuggestionsPopup { - background-color: #202228; -} - PopupItem { background-color: #202228; qproperty-hoverColor: rgba(45, 49, 57, 120); } -RoomList, -RoomList > * { - background-color: #2d3139; - border: none; -} - TypingDisplay { qproperty-textColor: #caccd1; qproperty-backgroundColor: #202228; @@ -61,35 +104,26 @@ TypingDisplay { background-color: #2d3139; } -CommunitiesList, -CommunitiesList > * { - background-color: #2d3139; -} - FlatButton { qproperty-foregroundColor: #727274; qproperty-backgroundColor: #333; qproperty-disabledForegroundColor: #222; } +AudioItem, FileItem { qproperty-textColor: #caccd1; qproperty-backgroundColor: #2d3139; qproperty-iconColor: #caccd1; } -AudioItem { - qproperty-textColor: #caccd1; - qproperty-backgroundColor: #2d3139; - qproperty-iconColor: #caccd1; -} - RaisedButton { qproperty-foregroundColor: #caccd1; qproperty-backgroundColor: #333; } -RoomInfoListItem { +RoomInfoListItem, +UserMentionsWidget { qproperty-mentionedColor: #a82353; qproperty-highlightedBackgroundColor: #4d84c7; qproperty-hoverBackgroundColor: rgba(230, 230, 230, 30); @@ -111,13 +145,15 @@ RoomInfoListItem { qproperty-highlightedTimestampColor: #e7e7e9; qproperty-hoverTimestampColor: #f4f5f8; - qproperty-avatarBgColor: #202228; - qproperty-avatarFgColor: white; - qproperty-bubbleFgColor: white; qproperty-bubbleBgColor: #4d84c7; } +RoomInfoListItem > Avatar { + qproperty-backgroundColor: #202228; + qproperty-textColor: white; +} + CommunitiesListItem { qproperty-highlightedBackgroundColor: #4d84c7; qproperty-hoverBackgroundColor: rgba(230, 230, 230, 30); @@ -141,12 +177,12 @@ UserInfoWidget { border-bottom: 1px solid #202228; } -UserSettingsPage { - background-color: #202228; +#UserSettingScrollWidget > QComboBox { + color: #202228; } -#UserSettingScrollWidget { - background-color: #202228; +#UserSettingScrollWidget > QComboBox { + color: #202228; } Avatar { @@ -154,55 +190,17 @@ Avatar { qproperty-backgroundColor: #2d3139; } -#displayNameLabel { - color: #f2f2f2; -} - +#displayNameLabel, #userIdLabel { color: #f2f2f2; } -dialogs--Logout, -dialogs--ReCaptcha, -dialogs--LeaveRoom, -dialogs--CreateRoom, -dialogs--RoomSettings, -dialogs--InviteUsers, -dialogs--ReadReceipts, -dialogs--JoinRoom, -dialogs--MemberList, -dialogs--PreviewUploadOverlay, -dialogs--UserProfile, -dialogs--CreateRoom > QLineEdit, -dialogs--InviteUsers > QLineEdit, -EditModal, -dialogs--JoinRoom > QLineEdit { - background-color: #202228; - color: #caccd1; -} - TopSection { qproperty-textColor: #caccd1; } -QListWidget, -WelcomePage, -LoginPage, -RegisterPage { - background-color: #202228; - color: #caccd1; -} - -emoji--Panel, -emoji--Panel > * { - background-color: #202228; - color: #caccd1; -} - -emoji--Category, -emoji--Category > * { - background-color: #2d3139; - color: #caccd1; +emoji--Category { + qproperty-hoverBackgroundColor: rgba(230, 230, 230, 30); } FloatingButton { @@ -221,23 +219,14 @@ ScrollBar { qproperty-backgroundColor: #202228; } -SideBarActions { +SideBarActions, +TopRoomBar +{ border: none; border-top: 1px solid #202228; background-color: #2d3139; } -TopRoomBar { - border: none; - border-bottom: 1px solid #202228; - background-color: #2d3139; -} - -QLineEdit { - background-color: #202228; - color: #caccd1; -} - TextInputWidget { border: none; border-top: 1px solid #2d3139; @@ -261,3 +250,5 @@ SnackBar { qproperty-textColor: #caccd1; qproperty-bgColor: #202228; } + +QSplitter::handle { image: none; } diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index 58e83c22..4c59bad1 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -3,33 +3,49 @@ QLabel { color: #333; } -TimelineItem { - qproperty-backgroundColor: white; +TextLabel::a { + color: #0077b5; } -#chatPage, -#chatPage > * { - background-color: white; -} -#sideBar { - border: none; - border-right: 1px solid #dee1f3; - border-left: 1px solid #dee1f3; +QuickSwitcher, +ReplyPopup, +SuggestionsPopup, +UserSettingsPage, +#scroll_widget, +#UserSettingScrollWidget { + background-color: white; } +#chatPage, +#chatPage > *, +CommunitiesList, +CommunitiesList > *, +RoomList, +RoomList > *, +TimelineView, +TimelineView > *, +UserMentionsWidget, +UserMentionsWidget > *, TimelineView, TimelineView > * { background-color: white; border: none; } -#scroll_widget { - background-color: white; +TimelineItem { + qproperty-backgroundColor: white; } -QuickSwitcher { - background-color: white; +#sideBar { + border: none; + border-right: 1px solid #dee1f3; + border-left: 1px solid #dee1f3; +} + +UserMentionsWidget > TimelineItem { + qproperty-backgroundColor: white; + qproperty-hoverColor: rgba(192, 193, 195, 120); } InfoMessage { @@ -42,17 +58,15 @@ TypingDisplay { qproperty-backgroundColor: white; } -SuggestionsPopup { - background-color: white; -} - PopupItem { background-color: white; qproperty-hoverColor: rgba(192, 193, 195, 120); } RoomList, -RoomList > * { +RoomList > *, +CommunitiesList, +CommunitiesList > * { background-color: #2e3649; border: none; } @@ -61,27 +75,17 @@ RoomList > * { background-color: #2e3649; } -CommunitiesList, -CommunitiesList > * { - background-color: #2e3649; -} - FlatButton { qproperty-foregroundColor: #495057; } +AudioItem, FileItem { qproperty-textColor: #333; qproperty-backgroundColor: #f2f2f2; qproperty-iconColor: white; } -AudioItem { - qproperty-textColor: #333; - qproperty-backgroundColor: #f2f2f2; - qproperty-iconColor: white; -} - RaisedButton { qproperty-foregroundColor: white; } @@ -89,7 +93,7 @@ RaisedButton { RoomInfoListItem { qproperty-mentionedColor: #a82353; qproperty-highlightedBackgroundColor: #38A3D8; - qproperty-hoverBackgroundColor: rgba(200, 200, 200, 70); + qproperty-hoverBackgroundColor: rgba(200, 200, 200, 40); qproperty-hoverTitleColor: #f2f5f8; qproperty-hoverSubtitleColor: white; qproperty-backgroundColor: #f2f5f8; @@ -107,16 +111,18 @@ RoomInfoListItem { qproperty-highlightedTimestampColor: #f4f4f5; qproperty-hoverTimestampColor: white; - qproperty-avatarBgColor: #eee; - qproperty-avatarFgColor: black; - qproperty-bubbleFgColor: white; qproperty-bubbleBgColor: #38A3D8; } +RoomInfoListItem > Avatar { + qproperty-backgroundColor: #eee; + qproperty-textColor: black; +} + CommunitiesListItem { qproperty-highlightedBackgroundColor: #38A3D8; - qproperty-hoverBackgroundColor: rgba(200, 200, 200, 70); + qproperty-hoverBackgroundColor: rgba(200, 200, 200, 40); qproperty-backgroundColor: #f2f5f8; qproperty-avatarBgColor: #eee; @@ -141,14 +147,6 @@ UserInfoWidget { border-bottom: 2px solid #ccc; } -UserSettingsPage { - background-color: white; -} - -#UserSettingScrollWidget { - background-color: white; -} - Avatar { qproperty-textColor: black; qproperty-backgroundColor: #eee; @@ -196,12 +194,22 @@ emoji--Panel > * { color: #333; } +emoji--Panel QWidget { border: none; } +emoji--Panel QScrollBar:vertical { width: 0px; margin: 0px; } +emoji--Panel QScrollBar::handle:vertical { min-height: 30px; } + +emoji--Category { + qproperty-hoverBackgroundColor: rgba(200, 200, 200, 70); +} + emoji--Category, emoji--Category > * { background-color: white; color: #ccc; } +emoji--Category QLabel { margin: 20px 0 20px 8px; } + FloatingButton { qproperty-backgroundColor: #efefef; qproperty-foregroundColor: black; @@ -244,3 +252,5 @@ SnackBar { qproperty-textColor: white; qproperty-bgColor: #495057; } + +QSplitter::handle { image: none; } diff --git a/resources/styles/system.qss b/resources/styles/system.qss index c1e8898a..3ae3147a 100644 --- a/resources/styles/system.qss +++ b/resources/styles/system.qss @@ -1,3 +1,16 @@ +#chatPage, +#chatPage > *, +CommunitiesList, +CommunitiesList > *, +RoomList, +RoomList > *, +TimelineView, +TimelineView > *, +UserMentionsWidget, +UserMentionsWidget > * { + border: none; +} + TypingDisplay { qproperty-textColor: palette(text); qproperty-backgroundColor: palette(window); @@ -7,21 +20,18 @@ TimelineItem { qproperty-backgroundColor: palette(window); } -TimelineView, -TimelineView > * { - border: none; +UserMentionsWidget > TimelineItem { + qproperty-backgroundColor: palette(window); + qproperty-hoverColor: palette(base); } +SideBarActions, TextInputWidget { border: none; border-top: 1px solid palette(mid); } -SideBarActions { - border: none; - border-top: 1px solid palette(mid); -} - +UserInfoWidget, TopRoomBar { border: none; border-bottom: 1px solid palette(mid); @@ -33,11 +43,6 @@ RoomList > * { border: none; } -UserInfoWidget { - border: none; - border-bottom: 1px solid palette(mid); -} - #sideBar { border: none; border-right: 1px solid palette(mid); @@ -57,18 +62,13 @@ FlatButton { qproperty-foregroundColor: palette(text); } +AudioItem, FileItem { qproperty-textColor: palette(text); qproperty-backgroundColor: palette(base); qproperty-iconColor: palette(window); } -AudioItem { - qproperty-textColor: palette(text); - qproperty-backgroundColor: palette(base); - qproperty-iconColor: palette(window); -} - RaisedButton { qproperty-foregroundColor: palette(buttonText); } @@ -85,10 +85,11 @@ QListWidget { background-color: palette(window); } -RoomInfoListItem { +RoomInfoListItem, +UserMentionsWidget { qproperty-mentionedColor: palette(alternate-base); qproperty-highlightedBackgroundColor: palette(highlight); - qproperty-hoverBackgroundColor: palette(base); + qproperty-hoverBackgroundColor: palette(light); qproperty-backgroundColor: palette(window); qproperty-titleColor: palette(text); @@ -107,16 +108,19 @@ RoomInfoListItem { qproperty-highlightedTimestampColor: palette(highlightedtext); qproperty-hoverTimestampColor: palette(highlightedtext); - qproperty-avatarBgColor: palette(base); - qproperty-avatarFgColor: palette(text); - qproperty-bubbleBgColor: palette(base); qproperty-bubbleFgColor: palette(text); } +RoomInfoListItem > Avatar { + qproperty-backgroundColor: palette(base); + qproperty-textColor: palette(text); +} + + CommunitiesListItem { qproperty-highlightedBackgroundColor: palette(highlight); - qproperty-hoverBackgroundColor: palette(base); + qproperty-hoverBackgroundColor: palette(light); qproperty-backgroundColor: palette(window); qproperty-avatarBgColor: palette(base); @@ -131,6 +135,30 @@ LoadingIndicator { qproperty-color: palette(light); } +emoji--Panel, +emoji--Panel > * { + background-color: palette(base); + color: palette(text); +} + +emoji--Panel QWidget { border: none; } +emoji--Panel QScrollBar:vertical { width: 0px; margin: 0px; } +emoji--Panel QScrollBar::handle:vertical { min-height: 30px; } + +emoji--Category { + qproperty-hoverBackgroundColor: palette(highlight); +} + +emoji--Category, +emoji--Category > * { + background-color: palette(window); + color: palette(text); +} + +emoji--Category QLabel { + margin: 20px 0 20px 8px; +} + FloatingButton { qproperty-backgroundColor: palette(base); qproperty-foregroundColor: palette(text); @@ -141,13 +169,11 @@ SnackBar { qproperty-bgColor: palette(base); } -MemberItem { - background-color: palette(window); -} - Toggle { qproperty-activeColor: palette(highlight); qproperty-disabledColor: palette(dark); qproperty-inactiveColor: palette(mid); qproperty-trackColor: palette(base); } + +QSplitter::handle { image: none; } diff --git a/scripts/emoji_codegen.py b/scripts/emoji_codegen.py index cfa72425..634887b2 100755 --- a/scripts/emoji_codegen.py +++ b/scripts/emoji_codegen.py @@ -1,31 +1,19 @@ #!/usr/bin/env python3 import sys -import json +import re from jinja2 import Template class Emoji(object): - def __init__(self, code, shortname, category, order): - self.code = ''.join(list(map(code_to_bytes, code.split('-')))) + def __init__(self, code, shortname): + self.code = repr(code.encode('utf-8'))[1:].strip("'") self.shortname = shortname - self.category = category - self.order = int(order) - - -def code_to_bytes(codepoint): - ''' - Convert hex unicode codepoint to hex byte array. - ''' - bytes = chr(int(codepoint, 16)).encode('utf-8') - - return str(bytes)[1:].strip("'") - def generate_code(emojis, category): tmpl = Template(''' -const QList<Emoji> EmojiProvider::{{ category }} = { +const std::vector<Emoji> emoji::Provider::{{ category }} = { {%- for e in emoji %} Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}"}, {%- endfor %} @@ -38,44 +26,56 @@ const QList<Emoji> EmojiProvider::{{ category }} = { if __name__ == '__main__': if len(sys.argv) < 2: - print('usage: emoji_codegen.py /path/to/emoji.json') + print('usage: emoji_codegen.py /path/to/emoji-test.txt') sys.exit(1) filename = sys.argv[1] - data = {} - with open(filename, 'r') as filename: - data = json.loads(filename.read()) + people = [] + nature = [] + food = [] + activity = [] + travel = [] + objects = [] + symbols = [] + flags = [] + + categories = { + 'Smileys & Emotion': people, + 'People & Body': people, + 'Animals & Nature': nature, + 'Food & Drink': food, + 'Travel & Places': travel, + 'Activities': activity, + 'Objects': objects, + 'Symbols': symbols, + 'Flags': flags + } + + current_category = '' + for line in open(filename, 'r'): + if line.startswith('# group:'): + current_category = line.split(':', 1)[1].strip() + + if not line or line.startswith('#'): + continue - emojis = [] + segments = re.split(r'\s+[#;] ', line.strip()) + if len(segments) != 3: + continue - for emoji_name in data: - tmp = data[emoji_name] + code, qualification, charAndName = segments - l = len(tmp['unicode'].split('-')) + # skip fully qualified versions of same unicode + if code.endswith('FE0F'): + continue - if l > 1 and tmp['category'] == 'people': + if qualification == 'component': continue - emojis.append( - Emoji( - tmp['unicode'], - tmp['shortname'], - tmp['category'], - tmp['emoji_order'] - ) - ) - - emojis.sort(key=lambda x: x.order) - - people = list(filter(lambda x: x.category == "people", emojis)) - nature = list(filter(lambda x: x.category == "nature", emojis)) - food = list(filter(lambda x: x.category == "food", emojis)) - activity = list(filter(lambda x: x.category == "activity", emojis)) - travel = list(filter(lambda x: x.category == "travel", emojis)) - objects = list(filter(lambda x: x.category == "objects", emojis)) - symbols = list(filter(lambda x: x.category == "symbols", emojis)) - flags = list(filter(lambda x: x.category == "flags", emojis)) + char, name = re.match(r'^(\S+) E\d+\.\d+ (.*)$', charAndName).groups() + + categories[current_category].append(Emoji(char, name)) # Use xclip to pipe the output to clipboard. # e.g ./codegen.py emoji.json | xclip -sel clip diff --git a/scripts/update_emoji.md b/scripts/update_emoji.md new file mode 100644 index 00000000..00fe8c4e --- /dev/null +++ b/scripts/update_emoji.md @@ -0,0 +1,7 @@ +# Updating emoji + +1. Get the latest emoji-test.txt from here: https://unicode.org/Public/emoji/ +2. Overwrite the existing resources/emoji-test.txt with the new one +3. Run `./scripts/emoji_codegen.py resources/emoji-test.txt` and replace the current tail of src/emoji/Provider.cpp with the new output +4. `make lint` +5. Compile and test diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index 57b61c75..d0556f85 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -16,30 +16,37 @@ */ #include <QBuffer> +#include <QPixmapCache> #include <memory> +#include <unordered_map> #include "AvatarProvider.h" #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" -namespace AvatarProvider { +static QPixmapCache avatar_cache; +namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback) +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback) { - const auto key = QString("%1 %2").arg(room_id).arg(user_id); - const auto avatarUrl = Cache::avatarUrl(room_id, user_id); + const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); - if (!Cache::AvatarUrls.contains(key) || !cache::client()) + if (avatarUrl.isEmpty()) return; - if (avatarUrl.isEmpty()) + QPixmap pixmap; + if (avatar_cache.find(cacheKey, &pixmap)) { + callback(pixmap); return; + } - auto data = cache::client()->image(avatarUrl); + auto data = cache::image(cacheKey); if (!data.isNull()) { - callback(QImage::fromData(data)); + pixmap.loadFromData(data); + avatar_cache.insert(cacheKey, pixmap); + callback(pixmap); return; } @@ -47,16 +54,22 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata QObject::connect(proxy.get(), &AvatarProxy::avatarDownloaded, receiver, - [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); + [callback, cacheKey](const QByteArray &data) { + QPixmap pm; + pm.loadFromData(data); + avatar_cache.insert(cacheKey, pm); + callback(pm); + }); mtx::http::ThumbOpts opts; - opts.width = 256; - opts.height = 256; + opts.width = size; + opts.height = size; opts.mxc_url = avatarUrl.toStdString(); http::client()->get_thumbnail( opts, - [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) { + [opts, cacheKey, proxy = std::move(proxy)](const std::string &res, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to download avatar: {} - ({} {})", opts.mxc_url, @@ -65,10 +78,21 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata return; } - cache::client()->saveImage(opts.mxc_url, res); + cache::saveImage(cacheKey.toStdString(), res); - auto data = QByteArray(res.data(), res.size()); - emit proxy->avatarDownloaded(data); + emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); }); } + +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback callback) +{ + const auto avatarUrl = cache::avatarUrl(room_id, user_id); + + resolve(avatarUrl, size, receiver, callback); +} } diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h index 4b4e15e9..47ed028e 100644 --- a/src/AvatarProvider.h +++ b/src/AvatarProvider.h @@ -17,7 +17,7 @@ #pragma once -#include <QImage> +#include <QPixmap> #include <functional> class AvatarProxy : public QObject @@ -28,9 +28,15 @@ signals: void avatarDownloaded(const QByteArray &data); }; -using AvatarCallback = std::function<void(QImage)>; +using AvatarCallback = std::function<void(QPixmap)>; namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb); +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb); +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback cb); } diff --git a/src/Cache.cpp b/src/Cache.cpp index 81054ddc..0f33a276 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -17,17 +17,21 @@ #include <limits> #include <stdexcept> +#include <variant> #include <QByteArray> +#include <QCoreApplication> #include <QFile> #include <QHash> +#include <QMap> #include <QSettings> #include <QStandardPaths> -#include <boost/variant.hpp> #include <mtx/responses/common.hpp> #include "Cache.h" +#include "Cache_p.h" +#include "Logging.h" #include "Utils.h" //! Should be changed when a breaking change occurs in the cache format. @@ -35,13 +39,13 @@ static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.09.21"); static const std::string SECRET("secret"); -static const lmdb::val NEXT_BATCH_KEY("next_batch"); -static const lmdb::val OLM_ACCOUNT_KEY("olm_account"); -static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); +static lmdb::val NEXT_BATCH_KEY("next_batch"); +static lmdb::val OLM_ACCOUNT_KEY("olm_account"); +static lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); -constexpr size_t MAX_RESTORED_MESSAGES = 30; +constexpr size_t MAX_RESTORED_MESSAGES = 30'000; -constexpr auto DB_SIZE = 512UL * 1024UL * 1024UL; // 512 MB +constexpr auto DB_SIZE = 32ULL * 1024ULL * 1024ULL * 1024ULL; // 32 GB constexpr auto MAX_DBS = 8092UL; //! Cache databases and their format. @@ -76,32 +80,30 @@ constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; +Q_DECLARE_METATYPE(SearchResult) +Q_DECLARE_METATYPE(std::vector<SearchResult>) +Q_DECLARE_METATYPE(RoomMember) +Q_DECLARE_METATYPE(mtx::responses::Timeline) +Q_DECLARE_METATYPE(RoomSearchResult) +Q_DECLARE_METATYPE(RoomInfo) + namespace { std::unique_ptr<Cache> instance_ = nullptr; } -namespace cache { -void -init(const QString &user_id) +int +numeric_key_comparison(const MDB_val *a, const MDB_val *b) { - qRegisterMetaType<SearchResult>(); - qRegisterMetaType<QVector<SearchResult>>(); - qRegisterMetaType<RoomMember>(); - qRegisterMetaType<RoomSearchResult>(); - qRegisterMetaType<RoomInfo>(); - qRegisterMetaType<QMap<QString, RoomInfo>>(); - qRegisterMetaType<std::map<QString, RoomInfo>>(); - qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>(); + auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); + auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); - instance_ = std::make_unique<Cache>(user_id); -} + if (lhs < rhs) + return 1; + else if (lhs == rhs) + return 0; -Cache * -client() -{ - return instance_.get(); + return -1; } -} // namespace cache Cache::Cache(const QString &userId, QObject *parent) : QObject{parent} @@ -392,7 +394,7 @@ Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr txn.commit(); } -boost::optional<mtx::crypto::OlmSessionPtr> +std::optional<mtx::crypto::OlmSessionPtr> Cache::getOlmSession(const std::string &curve25519, const std::string &session_id) { using namespace mtx::crypto; @@ -410,7 +412,7 @@ Cache::getOlmSession(const std::string &curve25519, const std::string &session_i return unpickle<SessionObject>(data, SECRET); } - return boost::none; + return std::nullopt; } std::vector<std::string> @@ -913,13 +915,17 @@ Cache::calculateRoomReadStatus(const std::string &room_id) auto txn = lmdb::txn::begin(env_); // Get last event id on the room. - const auto last_event_id = getLastMessageInfo(txn, room_id).event_id; + const auto last_event_id = getLastEventId(txn, room_id); const auto localUser = utils::localUser().toStdString(); txn.commit(); + if (last_event_id.empty()) + return false; + // Retrieve all read receipts for that event. - const auto receipts = readReceipts(last_event_id, QString::fromStdString(room_id)); + const auto receipts = + readReceipts(QString::fromStdString(last_event_id), QString::fromStdString(room_id)); if (receipts.size() == 0) return true; @@ -958,13 +964,14 @@ Cache::saveState(const mtx::responses::Sync &res) updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first)) .toStdString(); + updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); // Process the account_data associated with this room bool has_new_tags = false; for (const auto &evt : room.second.account_data.events) { // for now only fetch tag events - if (evt.type() == typeid(Event<account_data::Tag>)) { - auto tags_evt = boost::get<Event<account_data::Tag>>(evt); + if (std::holds_alternative<Event<account_data::Tag>>(evt)) { + auto tags_evt = std::get<Event<account_data::Tag>>(evt); has_new_tags = true; for (const auto &tag : tags_evt.content.tags) { updatedInfo.tags.push_back(tag.first); @@ -1045,19 +1052,17 @@ Cache::saveInvite(lmdb::txn &txn, using namespace mtx::events::state; for (const auto &e : room.invite_state) { - if (boost::get<StrippedEvent<Member>>(&e) != nullptr) { - auto msg = boost::get<StrippedEvent<Member>>(e); - - auto display_name = msg.content.display_name.empty() - ? msg.state_key - : msg.content.display_name; + if (auto msg = std::get_if<StrippedEvent<Member>>(&e)) { + auto display_name = msg->content.display_name.empty() + ? msg->state_key + : msg->content.display_name; - MemberInfo tmp{display_name, msg.content.avatar_url}; + MemberInfo tmp{display_name, msg->content.avatar_url}; lmdb::dbi_put( - txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump())); + txn, membersdb, lmdb::val(msg->state_key), lmdb::val(json(tmp).dump())); } else { - boost::apply_visitor( + std::visit( [&txn, &statesdb](auto msg) { bool res = lmdb::dbi_put(txn, statesdb, @@ -1065,8 +1070,8 @@ Cache::saveInvite(lmdb::txn &txn, lmdb::val(json(msg).dump())); if (!res) - std::cout << "couldn't save data" << json(msg).dump() - << '\n'; + nhlog::db()->warn("couldn't save data: {}", + json(msg).dump()); }, e); } @@ -1118,7 +1123,7 @@ Cache::roomsWithTagUpdates(const mtx::responses::Sync &res) for (const auto &room : res.rooms.join) { bool hasUpdates = false; for (const auto &evt : room.second.account_data.events) { - if (evt.type() == typeid(Event<account_data::Tag>)) { + if (std::holds_alternative<Event<account_data::Tag>>(evt)) { hasUpdates = true; } } @@ -1230,9 +1235,31 @@ Cache::roomMessages() return msgs; } +QMap<QString, mtx::responses::Notifications> +Cache::getTimelineMentions() +{ + // TODO: Should be read-only, but getMentionsDb will attempt to create a DB + // if it doesn't exist, throwing an error. + auto txn = lmdb::txn::begin(env_, nullptr); + + QMap<QString, mtx::responses::Notifications> notifs; + + auto room_ids = getRoomIds(txn); + + for (const auto &room_id : room_ids) { + auto roomNotifs = getTimelineMentionsForRoom(txn, room_id); + notifs[QString::fromStdString(room_id)] = roomNotifs; + } + + txn.commit(); + + return notifs; +} + mtx::responses::Timeline Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id) { + // TODO(nico): Limit the messages returned by this maybe? auto db = getMessagesDb(txn, room_id); mtx::responses::Timeline timeline; @@ -1300,6 +1327,31 @@ Cache::roomInfo(bool withInvites) return result; } +std::string +Cache::getLastEventId(lmdb::txn &txn, const std::string &room_id) +{ + auto db = getMessagesDb(txn, room_id); + + if (db.size(txn) == 0) + return {}; + + std::string timestamp, msg; + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(timestamp, msg, MDB_NEXT)) { + auto obj = json::parse(msg); + + if (obj.count("event") == 0) + continue; + + cursor.close(); + return obj["event"]["event_id"]; + } + cursor.close(); + + return {}; +} + DescInfo Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) { @@ -1311,13 +1363,15 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) std::string timestamp, msg; QSettings settings; - auto local_user = settings.value("auth/user_id").toString(); + const auto local_user = utils::localUser(); auto cursor = lmdb::cursor::open(txn, db); while (cursor.get(timestamp, msg, MDB_NEXT)) { auto obj = json::parse(msg); - if (obj.count("event") == 0) + if (obj.count("event") == 0 || !(obj["event"]["type"] == "m.room.message" || + obj["event"]["type"] == "m.sticker" || + obj["event"]["type"] == "m.room.encrypted")) continue; mtx::events::collections::TimelineEvent event; @@ -1481,7 +1535,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) return "Empty Room"; } -JoinRule +mtx::events::state::JoinRule Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; @@ -1493,14 +1547,14 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) if (res) { try { - StateEvent<JoinRules> msg = + StateEvent<state::JoinRules> msg = json::parse(std::string(event.data(), event.size())); return msg.content.join_rule; } catch (const json::exception &e) { nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); } } - return JoinRule::Knock; + return state::JoinRule::Knock; } bool @@ -1552,6 +1606,32 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) } QString +Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCreate)), event); + + if (res) { + try { + StateEvent<Create> msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.room_version.empty()) + return QString::fromStdString(msg.content.room_version); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); + } + } + + nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); + return QString("1"); +} + +QString Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { using namespace mtx::events; @@ -1779,10 +1859,7 @@ Cache::searchRooms(const std::string &query, std::uint8_t max_items) std::vector<RoomSearchResult> results; for (auto it = items.begin(); it != end; it++) { - results.push_back( - RoomSearchResult{it->second.first, - it->second.second, - QImage::fromData(image(txn, it->second.second.avatar_url))}); + results.push_back(RoomSearchResult{it->second.first, it->second.second}); } txn.commit(); @@ -1790,7 +1867,7 @@ Cache::searchRooms(const std::string &query, std::uint8_t max_items) return results; } -QVector<SearchResult> +std::vector<SearchResult> Cache::searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) { std::multimap<int, std::pair<std::string, std::string>> items; @@ -1813,7 +1890,7 @@ Cache::searchUsers(const std::string &room_id, const std::string &query, std::ui else if (items.size() > 0) std::advance(end, items.size()); - QVector<SearchResult> results; + std::vector<SearchResult> results; for (auto it = items.begin(); it != end; it++) { const auto user = it->second; results.push_back(SearchResult{QString::fromStdString(user.first), @@ -1889,10 +1966,7 @@ Cache::saveTimelineMessages(lmdb::txn &txn, using namespace mtx::events::state; for (const auto &e : res.events) { - if (isStateEvent(e)) - continue; - - if (boost::get<RedactionEvent<msg::Redaction>>(&e) != nullptr) + if (std::holds_alternative<RedactionEvent<msg::Redaction>>(e)) continue; json obj = json::object(); @@ -1907,6 +1981,88 @@ Cache::saveTimelineMessages(lmdb::txn &txn, } } +mtx::responses::Notifications +Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id) +{ + auto db = getMentionsDb(txn, room_id); + + if (db.size(txn) == 0) { + return mtx::responses::Notifications{}; + } + + mtx::responses::Notifications notif; + std::string event_id, msg; + + auto cursor = lmdb::cursor::open(txn, db); + + while (cursor.get(event_id, msg, MDB_NEXT)) { + auto obj = json::parse(msg); + + if (obj.count("event") == 0) + continue; + + mtx::responses::Notification notification; + mtx::responses::from_json(obj, notification); + + notif.notifications.push_back(notification); + } + cursor.close(); + + std::reverse(notif.notifications.begin(), notif.notifications.end()); + + return notif; +} + +//! Add all notifications containing a user mention to the db. +void +Cache::saveTimelineMentions(const mtx::responses::Notifications &res) +{ + QMap<std::string, QList<mtx::responses::Notification>> notifsByRoom; + + // Sort into room-specific 'buckets' + for (const auto ¬if : res.notifications) { + json val = notif; + notifsByRoom[notif.room_id].push_back(notif); + } + + auto txn = lmdb::txn::begin(env_); + // Insert the entire set of mentions for each room at a time. + QMap<std::string, QList<mtx::responses::Notification>>::const_iterator it = + notifsByRoom.constBegin(); + auto end = notifsByRoom.constEnd(); + while (it != end) { + nhlog::db()->debug("Storing notifications for " + it.key()); + saveTimelineMentions(txn, it.key(), std::move(it.value())); + ++it; + } + + txn.commit(); +} + +void +Cache::saveTimelineMentions(lmdb::txn &txn, + const std::string &room_id, + const QList<mtx::responses::Notification> &res) +{ + auto db = getMentionsDb(txn, room_id); + + using namespace mtx::events; + using namespace mtx::events::state; + + for (const auto ¬if : res) { + const auto event_id = utils::event_id(notif.event); + + // double check that we have the correct room_id... + if (room_id.compare(notif.room_id) != 0) { + return; + } + + json obj = notif; + + lmdb::dbi_put(txn, db, lmdb::val(event_id), lmdb::val(obj.dump())); + } +} + void Cache::markSentNotification(const std::string &event_id) { @@ -2059,7 +2215,6 @@ 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) @@ -2091,16 +2246,6 @@ 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, @@ -2132,19 +2277,604 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) } void -Cache::insertUserColor(const QString &user_id, const QString &color_name) +to_json(json &j, const RoomInfo &info) +{ + j["name"] = info.name; + j["topic"] = info.topic; + j["avatar_url"] = info.avatar_url; + j["version"] = info.version; + j["is_invite"] = info.is_invite; + j["join_rule"] = info.join_rule; + j["guest_access"] = info.guest_access; + + if (info.member_count != 0) + j["member_count"] = info.member_count; + + if (info.tags.size() != 0) + j["tags"] = info.tags; +} + +void +from_json(const json &j, RoomInfo &info) { - UserColors.insert(user_id, color_name); + info.name = j.at("name"); + info.topic = j.at("topic"); + info.avatar_url = j.at("avatar_url"); + info.version = j.value( + "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString()); + info.is_invite = j.at("is_invite"); + info.join_rule = j.at("join_rule"); + info.guest_access = j.at("guest_access"); + + if (j.count("member_count")) + info.member_count = j.at("member_count"); + + if (j.count("tags")) + info.tags = j.at("tags").get<std::vector<std::string>>(); +} + +void +to_json(json &j, const ReadReceiptKey &key) +{ + j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; } void -Cache::removeUserColor(const QString &user_id) +from_json(const json &j, ReadReceiptKey &key) { - UserColors.remove(user_id); + key.event_id = j.at("event_id").get<std::string>(); + key.room_id = j.at("room_id").get<std::string>(); } void -Cache::clearUserColors() +to_json(json &j, const MemberInfo &info) { - UserColors.clear(); + j["name"] = info.name; + j["avatar_url"] = info.avatar_url; } + +void +from_json(const json &j, MemberInfo &info) +{ + info.name = j.at("name"); + info.avatar_url = j.at("avatar_url"); +} + +void +to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +{ + obj["session_id"] = msg.session_id; + obj["session_key"] = msg.session_key; + obj["message_index"] = msg.message_index; +} + +void +from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) +{ + msg.session_id = obj.at("session_id"); + msg.session_key = obj.at("session_key"); + msg.message_index = obj.at("message_index"); +} + +void +to_json(nlohmann::json &obj, const DevicePublicKeys &msg) +{ + obj["ed25519"] = msg.ed25519; + obj["curve25519"] = msg.curve25519; +} + +void +from_json(const nlohmann::json &obj, DevicePublicKeys &msg) +{ + msg.ed25519 = obj.at("ed25519"); + msg.curve25519 = obj.at("curve25519"); +} + +void +to_json(nlohmann::json &obj, const MegolmSessionIndex &msg) +{ + obj["room_id"] = msg.room_id; + obj["session_id"] = msg.session_id; + obj["sender_key"] = msg.sender_key; +} + +void +from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) +{ + msg.room_id = obj.at("room_id"); + msg.session_id = obj.at("session_id"); + msg.sender_key = obj.at("sender_key"); +} + +namespace cache { +void +init(const QString &user_id) +{ + qRegisterMetaType<SearchResult>(); + qRegisterMetaType<std::vector<SearchResult>>(); + qRegisterMetaType<RoomMember>(); + qRegisterMetaType<RoomSearchResult>(); + qRegisterMetaType<RoomInfo>(); + qRegisterMetaType<QMap<QString, RoomInfo>>(); + qRegisterMetaType<std::map<QString, RoomInfo>>(); + qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>(); + + instance_ = std::make_unique<Cache>(user_id); +} + +Cache * +client() +{ + return instance_.get(); +} + +std::string +displayName(const std::string &room_id, const std::string &user_id) +{ + return instance_->displayName(room_id, user_id); +} + +QString +displayName(const QString &room_id, const QString &user_id) +{ + return instance_->displayName(room_id, user_id); +} +QString +avatarUrl(const QString &room_id, const QString &user_id) +{ + return instance_->avatarUrl(room_id, user_id); +} + +void +removeDisplayName(const QString &room_id, const QString &user_id) +{ + instance_->removeDisplayName(room_id, user_id); +} +void +removeAvatarUrl(const QString &room_id, const QString &user_id) +{ + instance_->removeAvatarUrl(room_id, user_id); +} + +void +insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name) +{ + instance_->insertDisplayName(room_id, user_id, display_name); +} +void +insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) +{ + instance_->insertAvatarUrl(room_id, user_id, avatar_url); +} + +//! Load saved data for the display names & avatars. +void +populateMembers() +{ + instance_->populateMembers(); +} + +std::vector<std::string> +joinedRooms() +{ + return instance_->joinedRooms(); +} + +QMap<QString, RoomInfo> +roomInfo(bool withInvites) +{ + return instance_->roomInfo(withInvites); +} +std::map<QString, bool> +invites() +{ + return instance_->invites(); +} + +QString +getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + return instance_->getRoomName(txn, statesdb, membersdb); +} +mtx::events::state::JoinRule +getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomJoinRule(txn, statesdb); +} +bool +getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomGuestAccess(txn, statesdb); +} +QString +getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomTopic(txn, statesdb); +} +QString +getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb, const QString &room_id) +{ + return instance_->getRoomAvatarUrl(txn, statesdb, membersdb, room_id); +} + +QString +getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomVersion(txn, statesdb); +} + +std::vector<RoomMember> +getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) +{ + return instance_->getMembers(room_id, startIndex, len); +} + +void +saveState(const mtx::responses::Sync &res) +{ + instance_->saveState(res); +} +bool +isInitialized() +{ + return instance_->isInitialized(); +} + +std::string +nextBatchToken() +{ + return instance_->nextBatchToken(); +} + +void +deleteData() +{ + instance_->deleteData(); +} + +void +removeInvite(lmdb::txn &txn, const std::string &room_id) +{ + instance_->removeInvite(txn, room_id); +} +void +removeInvite(const std::string &room_id) +{ + instance_->removeInvite(room_id); +} +void +removeRoom(lmdb::txn &txn, const std::string &roomid) +{ + instance_->removeRoom(txn, roomid); +} +void +removeRoom(const std::string &roomid) +{ + instance_->removeRoom(roomid); +} +void +removeRoom(const QString &roomid) +{ + instance_->removeRoom(roomid.toStdString()); +} +void +setup() +{ + instance_->setup(); +} + +bool +isFormatValid() +{ + return instance_->isFormatValid(); +} +void +setCurrentFormat() +{ + instance_->setCurrentFormat(); +} + +std::map<QString, mtx::responses::Timeline> +roomMessages() +{ + return instance_->roomMessages(); +} + +QMap<QString, mtx::responses::Notifications> +getTimelineMentions() +{ + return instance_->getTimelineMentions(); +} + +//! Retrieve all the user ids from a room. +std::vector<std::string> +roomMembers(const std::string &room_id) +{ + return instance_->roomMembers(room_id); +} + +//! Check if the given user has power leve greater than than +//! lowest power level of the given events. +bool +hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + const std::string &room_id, + const std::string &user_id) +{ + return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); +} + +//! Retrieves the saved room avatar. +QImage +getRoomAvatar(const QString &id) +{ + return instance_->getRoomAvatar(id); +} +QImage +getRoomAvatar(const std::string &id) +{ + return instance_->getRoomAvatar(id); +} + +void +updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) +{ + instance_->updateReadReceipt(txn, room_id, receipts); +} + +UserReceipts +readReceipts(const QString &event_id, const QString &room_id) +{ + return instance_->readReceipts(event_id, room_id); +} + +//! Filter the events that have at least one read receipt. +std::vector<QString> +filterReadEvents(const QString &room_id, + const std::vector<QString> &event_ids, + const std::string &excluded_user) +{ + return instance_->filterReadEvents(room_id, event_ids, excluded_user); +} +//! Add event for which we are expecting some read receipts. +void +addPendingReceipt(const QString &room_id, const QString &event_id) +{ + instance_->addPendingReceipt(room_id, event_id); +} +void +removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id) +{ + instance_->removePendingReceipt(txn, room_id, event_id); +} +void +notifyForReadReceipts(const std::string &room_id) +{ + instance_->notifyForReadReceipts(room_id); +} +std::vector<QString> +pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id) +{ + return instance_->pendingReceiptsEvents(txn, room_id); +} + +QByteArray +image(const QString &url) +{ + return instance_->image(url); +} +QByteArray +image(lmdb::txn &txn, const std::string &url) +{ + return instance_->image(txn, url); +} +void +saveImage(const std::string &url, const std::string &data) +{ + instance_->saveImage(url, data); +} +void +saveImage(const QString &url, const QByteArray &data) +{ + instance_->saveImage(url, data); +} + +RoomInfo +singleRoomInfo(const std::string &room_id) +{ + return instance_->singleRoomInfo(room_id); +} +std::vector<std::string> +roomsWithStateUpdates(const mtx::responses::Sync &res) +{ + return instance_->roomsWithStateUpdates(res); +} +std::vector<std::string> +roomsWithTagUpdates(const mtx::responses::Sync &res) +{ + return instance_->roomsWithTagUpdates(res); +} +std::map<QString, RoomInfo> +getRoomInfo(const std::vector<std::string> &rooms) +{ + return instance_->getRoomInfo(rooms); +} + +//! Calculates which the read status of a room. +//! Whether all the events in the timeline have been read. +bool +calculateRoomReadStatus(const std::string &room_id) +{ + return instance_->calculateRoomReadStatus(room_id); +} +void +calculateRoomReadStatus() +{ + instance_->calculateRoomReadStatus(); +} + +std::vector<SearchResult> +searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) +{ + return instance_->searchUsers(room_id, query, max_items); +} +std::vector<RoomSearchResult> +searchRooms(const std::string &query, std::uint8_t max_items) +{ + return instance_->searchRooms(query, max_items); +} + +void +markSentNotification(const std::string &event_id) +{ + instance_->markSentNotification(event_id); +} +//! Removes an event from the sent notifications. +void +removeReadNotification(const std::string &event_id) +{ + instance_->removeReadNotification(event_id); +} +//! Check if we have sent a desktop notification for the given event id. +bool +isNotificationSent(const std::string &event_id) +{ + return instance_->isNotificationSent(event_id); +} + +//! Add all notifications containing a user mention to the db. +void +saveTimelineMentions(const mtx::responses::Notifications &res) +{ + instance_->saveTimelineMentions(res); +} + +//! Remove old unused data. +void +deleteOldMessages() +{ + instance_->deleteOldMessages(); +} +void +deleteOldData() noexcept +{ + instance_->deleteOldData(); +} +//! Retrieve all saved room ids. +std::vector<std::string> +getRoomIds(lmdb::txn &txn) +{ + return instance_->getRoomIds(txn); +} + +//! Mark a room that uses e2e encryption. +void +setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) +{ + instance_->setEncryptedRoom(txn, room_id); +} +bool +isRoomEncrypted(const std::string &room_id) +{ + return instance_->isRoomEncrypted(room_id); +} + +//! Check if a user is a member of the room. +bool +isRoomMember(const std::string &user_id, const std::string &room_id) +{ + return instance_->isRoomMember(user_id, room_id); +} + +// +// Outbound Megolm Sessions +// +void +saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session) +{ + instance_->saveOutboundMegolmSession(room_id, data, std::move(session)); +} +OutboundGroupSessionDataRef +getOutboundMegolmSession(const std::string &room_id) +{ + return instance_->getOutboundMegolmSession(room_id); +} +bool +outboundMegolmSessionExists(const std::string &room_id) noexcept +{ + return instance_->outboundMegolmSessionExists(room_id); +} +void +updateOutboundMegolmSession(const std::string &room_id, int message_index) +{ + instance_->updateOutboundMegolmSession(room_id, message_index); +} + +void +importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) +{ + instance_->importSessionKeys(keys); +} +mtx::crypto::ExportedSessionKeys +exportSessionKeys() +{ + return instance_->exportSessionKeys(); +} + +// +// Inbound Megolm Sessions +// +void +saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session) +{ + instance_->saveInboundMegolmSession(index, std::move(session)); +} +OlmInboundGroupSession * +getInboundMegolmSession(const MegolmSessionIndex &index) +{ + return instance_->getInboundMegolmSession(index); +} +bool +inboundMegolmSessionExists(const MegolmSessionIndex &index) +{ + return instance_->inboundMegolmSessionExists(index); +} + +// +// Olm Sessions +// +void +saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +{ + instance_->saveOlmSession(curve25519, std::move(session)); +} +std::vector<std::string> +getOlmSessions(const std::string &curve25519) +{ + return instance_->getOlmSessions(curve25519); +} +std::optional<mtx::crypto::OlmSessionPtr> +getOlmSession(const std::string &curve25519, const std::string &session_id) +{ + return instance_->getOlmSession(curve25519, session_id); +} + +void +saveOlmAccount(const std::string &pickled) +{ + instance_->saveOlmAccount(pickled); +} +std::string +restoreOlmAccount() +{ + return instance_->restoreOlmAccount(); +} + +void +restoreSessions() +{ + return instance_->restoreSessions(); +} +} // namespace cache diff --git a/src/Cache.h b/src/Cache.h index b9cf0aeb..bb042ea9 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -17,716 +17,280 @@ #pragma once -#include <boost/optional.hpp> - #include <QDateTime> #include <QDir> #include <QImage> #include <QString> +#if __has_include(<lmdbxx/lmdb++.h>) +#include <lmdbxx/lmdb++.h> +#else #include <lmdb++.h> -#include <mtx/events/join_rules.hpp> -#include <mtx/responses.hpp> -#include <mtxclient/crypto/client.hpp> -#include <mutex> -#include <nlohmann/json.hpp> - -#include "Logging.h" - -using mtx::events::state::JoinRule; - -struct RoomMember -{ - QString user_id; - QString display_name; - QImage avatar; -}; +#endif -struct SearchResult -{ - QString user_id; - QString display_name; -}; +#include <mtx/responses.hpp> -static int -numeric_key_comparison(const MDB_val *a, const MDB_val *b) -{ - auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); - auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); +#include "CacheCryptoStructs.h" +#include "CacheStructs.h" - if (lhs < rhs) - return 1; - else if (lhs == rhs) - return 0; +namespace cache { +void +init(const QString &user_id); - return -1; -} +std::string +displayName(const std::string &room_id, const std::string &user_id); +QString +displayName(const QString &room_id, const QString &user_id); +QString +avatarUrl(const QString &room_id, const QString &user_id); -Q_DECLARE_METATYPE(SearchResult) -Q_DECLARE_METATYPE(QVector<SearchResult>) -Q_DECLARE_METATYPE(RoomMember) -Q_DECLARE_METATYPE(mtx::responses::Timeline) +void +removeDisplayName(const QString &room_id, const QString &user_id); +void +removeAvatarUrl(const QString &room_id, const QString &user_id); -//! Used to uniquely identify a list of read receipts. -struct ReadReceiptKey -{ - std::string event_id; - std::string room_id; -}; +void +insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name); +void +insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); -inline void -to_json(json &j, const ReadReceiptKey &key) -{ - j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; -} +//! Load saved data for the display names & avatars. +void +populateMembers(); +std::vector<std::string> +joinedRooms(); + +QMap<QString, RoomInfo> +roomInfo(bool withInvites = true); +std::map<QString, bool> +invites(); + +//! Calculate & return the name of the room. +QString +getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); +//! Get room join rules +mtx::events::state::JoinRule +getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); +bool +getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); +//! Retrieve the topic of the room if any. +QString +getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); +//! Retrieve the room avatar's url if any. +QString +getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb, const QString &room_id); +//! Retrieve the version of the room if any. +QString +getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + +//! Retrieve member info from a room. +std::vector<RoomMember> +getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); -inline void -from_json(const json &j, ReadReceiptKey &key) -{ - key.event_id = j.at("event_id").get<std::string>(); - key.room_id = j.at("room_id").get<std::string>(); -} +void +saveState(const mtx::responses::Sync &res); +bool +isInitialized(); -struct DescInfo -{ - QString event_id; - QString username; - QString userid; - QString body; - QString timestamp; - QDateTime datetime; -}; - -//! UI info associated with a room. -struct RoomInfo -{ - //! The calculated name of the room. - std::string name; - //! The topic of the room. - std::string topic; - //! The calculated avatar url of the room. - std::string avatar_url; - //! Whether or not the room is an invite. - bool is_invite = false; - //! Total number of members in the room. - int16_t member_count = 0; - //! Who can access to the room. - JoinRule join_rule = JoinRule::Public; - bool guest_access = false; - //! Metadata describing the last message in the timeline. - DescInfo msgInfo; - //! The list of tags associated with this room - std::vector<std::string> tags; -}; - -inline void -to_json(json &j, const RoomInfo &info) -{ - j["name"] = info.name; - j["topic"] = info.topic; - j["avatar_url"] = info.avatar_url; - j["is_invite"] = info.is_invite; - j["join_rule"] = info.join_rule; - j["guest_access"] = info.guest_access; - - if (info.member_count != 0) - j["member_count"] = info.member_count; - - if (info.tags.size() != 0) - j["tags"] = info.tags; -} +std::string +nextBatchToken(); -inline void -from_json(const json &j, RoomInfo &info) -{ - info.name = j.at("name"); - info.topic = j.at("topic"); - info.avatar_url = j.at("avatar_url"); - info.is_invite = j.at("is_invite"); - info.join_rule = j.at("join_rule"); - info.guest_access = j.at("guest_access"); - - if (j.count("member_count")) - info.member_count = j.at("member_count"); - - if (j.count("tags")) - info.tags = j.at("tags").get<std::vector<std::string>>(); -} +void +deleteData(); -//! Basic information per member; -struct MemberInfo -{ - std::string name; - std::string avatar_url; -}; +void +removeInvite(lmdb::txn &txn, const std::string &room_id); +void +removeInvite(const std::string &room_id); +void +removeRoom(lmdb::txn &txn, const std::string &roomid); +void +removeRoom(const std::string &roomid); +void +removeRoom(const QString &roomid); +void +setup(); -inline void -to_json(json &j, const MemberInfo &info) +bool +isFormatValid(); +void +setCurrentFormat(); + +std::map<QString, mtx::responses::Timeline> +roomMessages(); + +QMap<QString, mtx::responses::Notifications> +getTimelineMentions(); + +//! Retrieve all the user ids from a room. +std::vector<std::string> +roomMembers(const std::string &room_id); + +//! Check if the given user has power leve greater than than +//! lowest power level of the given events. +bool +hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + const std::string &room_id, + const std::string &user_id); + +//! Retrieves the saved room avatar. +QImage +getRoomAvatar(const QString &id); +QImage +getRoomAvatar(const std::string &id); + +//! Adds a user to the read list for the given event. +//! +//! There should be only one user id present in a receipt list per room. +//! The user id should be removed from any other lists. +using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; +void +updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts); + +//! Retrieve all the read receipts for the given event id and room. +//! +//! Returns a map of user ids and the time of the read receipt in milliseconds. +using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; +UserReceipts +readReceipts(const QString &event_id, const QString &room_id); + +//! Filter the events that have at least one read receipt. +std::vector<QString> +filterReadEvents(const QString &room_id, + const std::vector<QString> &event_ids, + const std::string &excluded_user); +//! Add event for which we are expecting some read receipts. +void +addPendingReceipt(const QString &room_id, const QString &event_id); +void +removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id); +void +notifyForReadReceipts(const std::string &room_id); +std::vector<QString> +pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); + +QByteArray +image(const QString &url); +QByteArray +image(lmdb::txn &txn, const std::string &url); +inline QByteArray +image(const std::string &url) { - j["name"] = info.name; - j["avatar_url"] = info.avatar_url; + return image(QString::fromStdString(url)); } - -inline void -from_json(const json &j, MemberInfo &info) +void +saveImage(const std::string &url, const std::string &data); +void +saveImage(const QString &url, const QByteArray &data); + +RoomInfo +singleRoomInfo(const std::string &room_id); +std::vector<std::string> +roomsWithStateUpdates(const mtx::responses::Sync &res); +std::vector<std::string> +roomsWithTagUpdates(const mtx::responses::Sync &res); +std::map<QString, RoomInfo> +getRoomInfo(const std::vector<std::string> &rooms); +inline std::map<QString, RoomInfo> +roomUpdates(const mtx::responses::Sync &sync) { - info.name = j.at("name"); - info.avatar_url = j.at("avatar_url"); + return getRoomInfo(roomsWithStateUpdates(sync)); } - -struct RoomSearchResult -{ - std::string room_id; - RoomInfo info; - QImage img; -}; - -Q_DECLARE_METATYPE(RoomSearchResult) -Q_DECLARE_METATYPE(RoomInfo) - -// Extra information associated with an outbound megolm session. -struct OutboundGroupSessionData -{ - std::string session_id; - std::string session_key; - uint64_t message_index = 0; -}; - -inline void -to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +inline std::map<QString, RoomInfo> +roomTagUpdates(const mtx::responses::Sync &sync) { - obj["session_id"] = msg.session_id; - obj["session_key"] = msg.session_key; - obj["message_index"] = msg.message_index; + return getRoomInfo(roomsWithTagUpdates(sync)); } -inline void -from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) -{ - msg.session_id = obj.at("session_id"); - msg.session_key = obj.at("session_key"); - msg.message_index = obj.at("message_index"); -} +//! Calculates which the read status of a room. +//! Whether all the events in the timeline have been read. +bool +calculateRoomReadStatus(const std::string &room_id); +void +calculateRoomReadStatus(); -struct OutboundGroupSessionDataRef -{ - OlmOutboundGroupSession *session; - OutboundGroupSessionData data; -}; +std::vector<SearchResult> +searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items = 5); +std::vector<RoomSearchResult> +searchRooms(const std::string &query, std::uint8_t max_items = 5); -struct DevicePublicKeys -{ - std::string ed25519; - std::string curve25519; -}; +void +markSentNotification(const std::string &event_id); +//! Removes an event from the sent notifications. +void +removeReadNotification(const std::string &event_id); +//! Check if we have sent a desktop notification for the given event id. +bool +isNotificationSent(const std::string &event_id); -inline void -to_json(nlohmann::json &obj, const DevicePublicKeys &msg) -{ - obj["ed25519"] = msg.ed25519; - obj["curve25519"] = msg.curve25519; -} +//! Add all notifications containing a user mention to the db. +void +saveTimelineMentions(const mtx::responses::Notifications &res); -inline void -from_json(const nlohmann::json &obj, DevicePublicKeys &msg) -{ - msg.ed25519 = obj.at("ed25519"); - msg.curve25519 = obj.at("curve25519"); -} +//! Remove old unused data. +void +deleteOldMessages(); +void +deleteOldData() noexcept; +//! Retrieve all saved room ids. +std::vector<std::string> +getRoomIds(lmdb::txn &txn); -//! Represents a unique megolm session identifier. -struct MegolmSessionIndex -{ - //! The room in which this session exists. - std::string room_id; - //! The session_id of the megolm session. - std::string session_id; - //! The curve25519 public key of the sender. - std::string sender_key; -}; - -inline void -to_json(nlohmann::json &obj, const MegolmSessionIndex &msg) -{ - obj["room_id"] = msg.room_id; - obj["session_id"] = msg.session_id; - obj["sender_key"] = msg.sender_key; -} +//! Mark a room that uses e2e encryption. +void +setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); +bool +isRoomEncrypted(const std::string &room_id); -inline void -from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) -{ - msg.room_id = obj.at("room_id"); - msg.session_id = obj.at("session_id"); - msg.sender_key = obj.at("sender_key"); -} +//! Check if a user is a member of the room. +bool +isRoomMember(const std::string &user_id, const std::string &room_id); -struct OlmSessionStorage -{ - // Megolm sessions - std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions; - std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions; - std::map<std::string, OutboundGroupSessionData> group_outbound_session_data; +// +// Outbound Megolm Sessions +// +void +saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); +OutboundGroupSessionDataRef +getOutboundMegolmSession(const std::string &room_id); +bool +outboundMegolmSessionExists(const std::string &room_id) noexcept; +void +updateOutboundMegolmSession(const std::string &room_id, int message_index); - // Guards for accessing megolm sessions. - std::mutex group_outbound_mtx; - std::mutex group_inbound_mtx; -}; +void +importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); +mtx::crypto::ExportedSessionKeys +exportSessionKeys(); -class Cache : public QObject -{ - Q_OBJECT - -public: - Cache(const QString &userId, QObject *parent = nullptr); - - 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, - const QString &display_name); - 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(); - std::vector<std::string> joinedRooms(); - - QMap<QString, RoomInfo> roomInfo(bool withInvites = true); - std::map<QString, bool> invites(); - - //! Calculate & return the name of the room. - QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - //! Get room join rules - JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); - bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the topic of the room if any. - QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the room avatar's url if any. - QString getRoomAvatarUrl(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const QString &room_id); - - //! Retrieve member info from a room. - std::vector<RoomMember> getMembers(const std::string &room_id, - std::size_t startIndex = 0, - std::size_t len = 30); - - void saveState(const mtx::responses::Sync &res); - bool isInitialized() const; - - std::string nextBatchToken() const; - - void deleteData(); - - void removeInvite(lmdb::txn &txn, const std::string &room_id); - void removeInvite(const std::string &room_id); - void removeRoom(lmdb::txn &txn, const std::string &roomid); - void removeRoom(const std::string &roomid); - void removeRoom(const QString &roomid) { removeRoom(roomid.toStdString()); }; - void setup(); - - bool isFormatValid(); - void setCurrentFormat(); - - std::map<QString, mtx::responses::Timeline> roomMessages(); - - //! Retrieve all the user ids from a room. - std::vector<std::string> roomMembers(const std::string &room_id); - - //! Check if the given user has power leve greater than than - //! lowest power level of the given events. - bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, - const std::string &room_id, - const std::string &user_id); - - //! Retrieves the saved room avatar. - QImage getRoomAvatar(const QString &id); - QImage getRoomAvatar(const std::string &id); - - //! Adds a user to the read list for the given event. - //! - //! There should be only one user id present in a receipt list per room. - //! The user id should be removed from any other lists. - using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; - void updateReadReceipt(lmdb::txn &txn, - const std::string &room_id, - const Receipts &receipts); - - //! Retrieve all the read receipts for the given event id and room. - //! - //! Returns a map of user ids and the time of the read receipt in milliseconds. - using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; - UserReceipts readReceipts(const QString &event_id, const QString &room_id); - - //! Filter the events that have at least one read receipt. - std::vector<QString> filterReadEvents(const QString &room_id, - const std::vector<QString> &event_ids, - const std::string &excluded_user); - //! Add event for which we are expecting some read receipts. - void addPendingReceipt(const QString &room_id, const QString &event_id); - void removePendingReceipt(lmdb::txn &txn, - const std::string &room_id, - const std::string &event_id); - void notifyForReadReceipts(const std::string &room_id); - std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); - - QByteArray image(const QString &url) const; - QByteArray image(lmdb::txn &txn, const std::string &url) const; - QByteArray image(const std::string &url) const - { - return image(QString::fromStdString(url)); - } - void saveImage(const std::string &url, const std::string &data); - void saveImage(const QString &url, const QByteArray &data); - - RoomInfo singleRoomInfo(const std::string &room_id); - std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); - std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res); - std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms); - std::map<QString, RoomInfo> roomUpdates(const mtx::responses::Sync &sync) - { - return getRoomInfo(roomsWithStateUpdates(sync)); - } - std::map<QString, RoomInfo> roomTagUpdates(const mtx::responses::Sync &sync) - { - return getRoomInfo(roomsWithTagUpdates(sync)); - } - - //! Calculates which the read status of a room. - //! Whether all the events in the timeline have been read. - bool calculateRoomReadStatus(const std::string &room_id); - void calculateRoomReadStatus(); - - QVector<SearchResult> searchUsers(const std::string &room_id, - const std::string &query, - std::uint8_t max_items = 5); - std::vector<RoomSearchResult> searchRooms(const std::string &query, - std::uint8_t max_items = 5); - - void markSentNotification(const std::string &event_id); - //! Removes an event from the sent notifications. - void removeReadNotification(const std::string &event_id); - //! Check if we have sent a desktop notification for the given event id. - bool isNotificationSent(const std::string &event_id); - - //! Remove old unused data. - void deleteOldMessages(); - void deleteOldData() noexcept; - //! Retrieve all saved room ids. - std::vector<std::string> getRoomIds(lmdb::txn &txn); - - //! Mark a room that uses e2e encryption. - void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); - bool isRoomEncrypted(const std::string &room_id); - - //! Save the public keys for a device. - void saveDeviceKeys(const std::string &device_id); - void getDeviceKeys(const std::string &device_id); - - //! Save the device list for a user. - void setDeviceList(const std::string &user_id, const std::vector<std::string> &devices); - std::vector<std::string> getDeviceList(const std::string &user_id); - - //! Check if a user is a member of the room. - bool isRoomMember(const std::string &user_id, const std::string &room_id); - - // - // Outbound Megolm Sessions - // - void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session); - OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); - bool outboundMegolmSessionExists(const std::string &room_id) noexcept; - void updateOutboundMegolmSession(const std::string &room_id, int message_index); - - void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); - mtx::crypto::ExportedSessionKeys exportSessionKeys(); - - // - // Inbound Megolm Sessions - // - void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session); - OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); - bool inboundMegolmSessionExists(const MegolmSessionIndex &index); - - // - // Olm Sessions - // - void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); - std::vector<std::string> getOlmSessions(const std::string &curve25519); - boost::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, - const std::string &session_id); - - void saveOlmAccount(const std::string &pickled); - std::string restoreOlmAccount(); - - void restoreSessions(); - - OlmSessionStorage session_storage; - -signals: - void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); - void roomReadStatus(const std::map<QString, bool> &status); - -private: - //! Save an invited room. - void saveInvite(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const mtx::responses::InvitedRoom &room); - - QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - - DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); - void saveTimelineMessages(lmdb::txn &txn, - const std::string &room_id, - const mtx::responses::Timeline &res); - - mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); - - //! Remove a room from the cache. - // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); - template<class T> - void saveStateEvents(lmdb::txn &txn, - const lmdb::dbi &statesdb, - const lmdb::dbi &membersdb, - const std::string &room_id, - const std::vector<T> &events) - { - for (const auto &e : events) - saveStateEvent(txn, statesdb, membersdb, room_id, e); - } - - template<class T> - void saveStateEvent(lmdb::txn &txn, - const lmdb::dbi &statesdb, - const lmdb::dbi &membersdb, - const std::string &room_id, - const T &event) - { - using namespace mtx::events; - using namespace mtx::events::state; - - if (boost::get<StateEvent<Member>>(&event) != nullptr) { - const auto e = boost::get<StateEvent<Member>>(event); - - switch (e.content.membership) { - // - // We only keep users with invite or join membership. - // - case Membership::Invite: - case Membership::Join: { - auto display_name = e.content.display_name.empty() - ? e.state_key - : e.content.display_name; - - // Lightweight representation of a member. - MemberInfo tmp{display_name, e.content.avatar_url}; - - lmdb::dbi_put(txn, - membersdb, - lmdb::val(e.state_key), - lmdb::val(json(tmp).dump())); - - insertDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e.state_key), - QString::fromStdString(display_name)); - - insertAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e.state_key), - QString::fromStdString(e.content.avatar_url)); - - break; - } - default: { - lmdb::dbi_del( - txn, membersdb, lmdb::val(e.state_key), lmdb::val("")); - - removeDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e.state_key)); - removeAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e.state_key)); - - break; - } - } - - return; - } else if (boost::get<StateEvent<Encryption>>(&event) != nullptr) { - setEncryptedRoom(txn, room_id); - return; - } - - if (!isStateEvent(event)) - return; - - boost::apply_visitor( - [&txn, &statesdb](auto e) { - lmdb::dbi_put( - txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); - }, - event); - } - - template<class T> - bool isStateEvent(const T &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return boost::get<StateEvent<Aliases>>(&e) != nullptr || - boost::get<StateEvent<state::Avatar>>(&e) != nullptr || - boost::get<StateEvent<CanonicalAlias>>(&e) != nullptr || - boost::get<StateEvent<Create>>(&e) != nullptr || - boost::get<StateEvent<GuestAccess>>(&e) != nullptr || - boost::get<StateEvent<HistoryVisibility>>(&e) != nullptr || - boost::get<StateEvent<JoinRules>>(&e) != nullptr || - boost::get<StateEvent<Name>>(&e) != nullptr || - boost::get<StateEvent<Member>>(&e) != nullptr || - boost::get<StateEvent<PowerLevels>>(&e) != nullptr || - boost::get<StateEvent<Topic>>(&e) != nullptr; - } - - template<class T> - bool containsStateUpdates(const T &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return boost::get<StateEvent<state::Avatar>>(&e) != nullptr || - boost::get<StateEvent<CanonicalAlias>>(&e) != nullptr || - boost::get<StateEvent<Name>>(&e) != nullptr || - boost::get<StateEvent<Member>>(&e) != nullptr || - boost::get<StateEvent<Topic>>(&e) != nullptr; - } - - bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return boost::get<StrippedEvent<state::Avatar>>(&e) != nullptr || - boost::get<StrippedEvent<CanonicalAlias>>(&e) != nullptr || - boost::get<StrippedEvent<Name>>(&e) != nullptr || - boost::get<StrippedEvent<Member>>(&e) != nullptr || - boost::get<StrippedEvent<Topic>>(&e) != nullptr; - } - - void saveInvites(lmdb::txn &txn, - const std::map<std::string, mtx::responses::InvitedRoom> &rooms); - - //! Sends signals for the rooms that are removed. - void removeLeftRooms(lmdb::txn &txn, - const std::map<std::string, mtx::responses::LeftRoom> &rooms) - { - for (const auto &room : rooms) { - removeRoom(txn, room.first); - - // Clean up leftover invites. - removeInvite(txn, room.first); - } - } - - lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) - { - return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); - } - - lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) - { - auto db = - lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); - lmdb::dbi_set_compare(txn, db, numeric_key_comparison); - - return db; - } - - lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); - } - - lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); - } - - lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); - } - - lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); - } - - //! Retrieves or creates the database that stores the open OLM sessions between our device - //! and the given curve25519 key which represents another device. - //! - //! Each entry is a map from the session_id to the pickled representation of the session. - lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) - { - return lmdb::dbi::open( - txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); - } - - QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) - { - if (!event.content.display_name.empty()) - return QString::fromStdString(event.content.display_name); - - return QString::fromStdString(event.state_key); - } - - void setNextBatchToken(lmdb::txn &txn, const std::string &token); - void setNextBatchToken(lmdb::txn &txn, const QString &token); - - lmdb::env env_; - lmdb::dbi syncStateDb_; - lmdb::dbi roomsDb_; - lmdb::dbi invitesDb_; - lmdb::dbi mediaDb_; - lmdb::dbi readReceiptsDb_; - lmdb::dbi notificationsDb_; - - lmdb::dbi devicesDb_; - lmdb::dbi deviceKeysDb_; - - lmdb::dbi inboundMegolmSessionDb_; - lmdb::dbi outboundMegolmSessionDb_; - - QString localUserId_; - QString cacheDirectory_; -}; +// +// Inbound Megolm Sessions +// +void +saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); +OlmInboundGroupSession * +getInboundMegolmSession(const MegolmSessionIndex &index); +bool +inboundMegolmSessionExists(const MegolmSessionIndex &index); + +// +// Olm Sessions +// +void +saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); +std::vector<std::string> +getOlmSessions(const std::string &curve25519); +std::optional<mtx::crypto::OlmSessionPtr> +getOlmSession(const std::string &curve25519, const std::string &session_id); -namespace cache { void -init(const QString &user_id); +saveOlmAccount(const std::string &pickled); +std::string +restoreOlmAccount(); -Cache * -client(); +void +restoreSessions(); } diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h new file mode 100644 index 00000000..14c9c86b --- /dev/null +++ b/src/CacheCryptoStructs.h @@ -0,0 +1,67 @@ +#pragma once + +#include <map> +#include <mutex> + +//#include <nlohmann/json.hpp> + +#include <mtx/responses.hpp> +#include <mtxclient/crypto/client.hpp> + +// Extra information associated with an outbound megolm session. +struct OutboundGroupSessionData +{ + std::string session_id; + std::string session_key; + uint64_t message_index = 0; +}; + +void +to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg); +void +from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg); + +struct OutboundGroupSessionDataRef +{ + OlmOutboundGroupSession *session; + OutboundGroupSessionData data; +}; + +struct DevicePublicKeys +{ + std::string ed25519; + std::string curve25519; +}; + +void +to_json(nlohmann::json &obj, const DevicePublicKeys &msg); +void +from_json(const nlohmann::json &obj, DevicePublicKeys &msg); + +//! Represents a unique megolm session identifier. +struct MegolmSessionIndex +{ + //! The room in which this session exists. + std::string room_id; + //! The session_id of the megolm session. + std::string session_id; + //! The curve25519 public key of the sender. + std::string sender_key; +}; + +void +to_json(nlohmann::json &obj, const MegolmSessionIndex &msg); +void +from_json(const nlohmann::json &obj, MegolmSessionIndex &msg); + +struct OlmSessionStorage +{ + // Megolm sessions + std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions; + std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions; + std::map<std::string, OutboundGroupSessionData> group_outbound_session_data; + + // Guards for accessing megolm sessions. + std::mutex group_outbound_mtx; + std::mutex group_inbound_mtx; +}; diff --git a/src/CacheStructs.h b/src/CacheStructs.h new file mode 100644 index 00000000..2051afc8 --- /dev/null +++ b/src/CacheStructs.h @@ -0,0 +1,91 @@ +#pragma once + +#include <QDateTime> +#include <QImage> +#include <QString> + +#include <string> + +#include <mtx/events/join_rules.hpp> + +struct RoomMember +{ + QString user_id; + QString display_name; + QImage avatar; +}; + +struct SearchResult +{ + QString user_id; + QString display_name; +}; + +//! Used to uniquely identify a list of read receipts. +struct ReadReceiptKey +{ + std::string event_id; + std::string room_id; +}; + +void +to_json(nlohmann::json &j, const ReadReceiptKey &key); + +void +from_json(const nlohmann::json &j, ReadReceiptKey &key); + +struct DescInfo +{ + QString event_id; + QString userid; + QString body; + QString timestamp; + QDateTime datetime; +}; + +//! UI info associated with a room. +struct RoomInfo +{ + //! The calculated name of the room. + std::string name; + //! The topic of the room. + std::string topic; + //! The calculated avatar url of the room. + std::string avatar_url; + //! The calculated version of this room set at creation time. + std::string version; + //! Whether or not the room is an invite. + bool is_invite = false; + //! Total number of members in the room. + int16_t member_count = 0; + //! Who can access to the room. + mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public; + bool guest_access = false; + //! Metadata describing the last message in the timeline. + DescInfo msgInfo; + //! The list of tags associated with this room + std::vector<std::string> tags; +}; + +void +to_json(nlohmann::json &j, const RoomInfo &info); +void +from_json(const nlohmann::json &j, RoomInfo &info); + +//! Basic information per member; +struct MemberInfo +{ + std::string name; + std::string avatar_url; +}; + +void +to_json(nlohmann::json &j, const MemberInfo &info); +void +from_json(const nlohmann::json &j, MemberInfo &info); + +struct RoomSearchResult +{ + std::string room_id; + RoomInfo info; +}; diff --git a/src/Cache_p.h b/src/Cache_p.h new file mode 100644 index 00000000..14ceafe8 --- /dev/null +++ b/src/Cache_p.h @@ -0,0 +1,490 @@ +/* + * nheko Copyright (C) 2019 The nheko authors + * 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 <optional> + +#include <QDateTime> +#include <QDir> +#include <QImage> +#include <QString> + +#if __has_include(<lmdbxx/lmdb++.h>) +#include <lmdbxx/lmdb++.h> +#else +#include <lmdb++.h> +#endif +#include <nlohmann/json.hpp> + +#include <mtx/responses.hpp> +#include <mtxclient/crypto/client.hpp> + +#include "CacheCryptoStructs.h" +#include "CacheStructs.h" + +int +numeric_key_comparison(const MDB_val *a, const MDB_val *b); + +class Cache : public QObject +{ + Q_OBJECT + +public: + Cache(const QString &userId, QObject *parent = nullptr); + + 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 void removeDisplayName(const QString &room_id, const QString &user_id); + static void removeAvatarUrl(const QString &room_id, const QString &user_id); + + static void insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name); + static void insertAvatarUrl(const QString &room_id, + const QString &user_id, + const QString &avatar_url); + + //! Load saved data for the display names & avatars. + void populateMembers(); + std::vector<std::string> joinedRooms(); + + QMap<QString, RoomInfo> roomInfo(bool withInvites = true); + std::map<QString, bool> invites(); + + //! Calculate & return the name of the room. + QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + //! Get room join rules + mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); + bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the topic of the room if any. + QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the room avatar's url if any. + QString getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id); + //! Retrieve the version of the room if any. + QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + + //! Retrieve member info from a room. + std::vector<RoomMember> getMembers(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); + + void saveState(const mtx::responses::Sync &res); + bool isInitialized() const; + + std::string nextBatchToken() const; + + void deleteData(); + + void removeInvite(lmdb::txn &txn, const std::string &room_id); + void removeInvite(const std::string &room_id); + void removeRoom(lmdb::txn &txn, const std::string &roomid); + void removeRoom(const std::string &roomid); + void setup(); + + bool isFormatValid(); + void setCurrentFormat(); + + std::map<QString, mtx::responses::Timeline> roomMessages(); + + QMap<QString, mtx::responses::Notifications> getTimelineMentions(); + + //! Retrieve all the user ids from a room. + std::vector<std::string> roomMembers(const std::string &room_id); + + //! Check if the given user has power leve greater than than + //! lowest power level of the given events. + bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + const std::string &room_id, + const std::string &user_id); + + //! Retrieves the saved room avatar. + QImage getRoomAvatar(const QString &id); + QImage getRoomAvatar(const std::string &id); + + //! Adds a user to the read list for the given event. + //! + //! There should be only one user id present in a receipt list per room. + //! The user id should be removed from any other lists. + using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; + void updateReadReceipt(lmdb::txn &txn, + const std::string &room_id, + const Receipts &receipts); + + //! Retrieve all the read receipts for the given event id and room. + //! + //! Returns a map of user ids and the time of the read receipt in milliseconds. + using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; + UserReceipts readReceipts(const QString &event_id, const QString &room_id); + + //! Filter the events that have at least one read receipt. + std::vector<QString> filterReadEvents(const QString &room_id, + const std::vector<QString> &event_ids, + const std::string &excluded_user); + //! Add event for which we are expecting some read receipts. + void addPendingReceipt(const QString &room_id, const QString &event_id); + void removePendingReceipt(lmdb::txn &txn, + const std::string &room_id, + const std::string &event_id); + void notifyForReadReceipts(const std::string &room_id); + std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); + + QByteArray image(const QString &url) const; + QByteArray image(lmdb::txn &txn, const std::string &url) const; + void saveImage(const std::string &url, const std::string &data); + void saveImage(const QString &url, const QByteArray &data); + + RoomInfo singleRoomInfo(const std::string &room_id); + std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); + std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res); + std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms); + + //! Calculates which the read status of a room. + //! Whether all the events in the timeline have been read. + bool calculateRoomReadStatus(const std::string &room_id); + void calculateRoomReadStatus(); + + std::vector<SearchResult> searchUsers(const std::string &room_id, + const std::string &query, + std::uint8_t max_items = 5); + std::vector<RoomSearchResult> searchRooms(const std::string &query, + std::uint8_t max_items = 5); + + void markSentNotification(const std::string &event_id); + //! Removes an event from the sent notifications. + void removeReadNotification(const std::string &event_id); + //! Check if we have sent a desktop notification for the given event id. + bool isNotificationSent(const std::string &event_id); + + //! Add all notifications containing a user mention to the db. + void saveTimelineMentions(const mtx::responses::Notifications &res); + + //! Remove old unused data. + void deleteOldMessages(); + void deleteOldData() noexcept; + //! Retrieve all saved room ids. + std::vector<std::string> getRoomIds(lmdb::txn &txn); + + //! Mark a room that uses e2e encryption. + void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); + bool isRoomEncrypted(const std::string &room_id); + + //! Check if a user is a member of the room. + bool isRoomMember(const std::string &user_id, const std::string &room_id); + + // + // Outbound Megolm Sessions + // + void saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); + OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); + bool outboundMegolmSessionExists(const std::string &room_id) noexcept; + void updateOutboundMegolmSession(const std::string &room_id, int message_index); + + void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); + mtx::crypto::ExportedSessionKeys exportSessionKeys(); + + // + // Inbound Megolm Sessions + // + void saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); + OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); + bool inboundMegolmSessionExists(const MegolmSessionIndex &index); + + // + // Olm Sessions + // + void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); + std::vector<std::string> getOlmSessions(const std::string &curve25519); + std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, + const std::string &session_id); + + void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); + + void restoreSessions(); + +signals: + void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); + void roomReadStatus(const std::map<QString, bool> &status); + +private: + //! Save an invited room. + void saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room); + + //! Add a notification containing a user mention to the db. + void saveTimelineMentions(lmdb::txn &txn, + const std::string &room_id, + const QList<mtx::responses::Notification> &res); + + //! Get timeline items that a user was mentions in for a given room + mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn, + const std::string &room_id); + + QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + + std::string getLastEventId(lmdb::txn &txn, const std::string &room_id); + DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); + void saveTimelineMessages(lmdb::txn &txn, + const std::string &room_id, + const mtx::responses::Timeline &res); + + mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); + + //! Remove a room from the cache. + // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); + template<class T> + void saveStateEvents(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const std::vector<T> &events) + { + for (const auto &e : events) + saveStateEvent(txn, statesdb, membersdb, room_id, e); + } + + template<class T> + void saveStateEvent(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const T &event) + { + using namespace mtx::events; + using namespace mtx::events::state; + + if (auto e = std::get_if<StateEvent<Member>>(&event); e != nullptr) { + switch (e->content.membership) { + // + // We only keep users with invite or join membership. + // + case Membership::Invite: + case Membership::Join: { + auto display_name = e->content.display_name.empty() + ? e->state_key + : e->content.display_name; + + // Lightweight representation of a member. + MemberInfo tmp{display_name, e->content.avatar_url}; + + lmdb::dbi_put(txn, + membersdb, + lmdb::val(e->state_key), + lmdb::val(json(tmp).dump())); + + insertDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e->state_key), + QString::fromStdString(display_name)); + + insertAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e->state_key), + QString::fromStdString(e->content.avatar_url)); + + break; + } + default: { + lmdb::dbi_del( + txn, membersdb, lmdb::val(e->state_key), lmdb::val("")); + + removeDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e->state_key)); + removeAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e->state_key)); + + break; + } + } + + return; + } else if (std::holds_alternative<StateEvent<Encryption>>(event)) { + setEncryptedRoom(txn, room_id); + return; + } + + if (!isStateEvent(event)) + return; + + std::visit( + [&txn, &statesdb](auto e) { + lmdb::dbi_put( + txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); + }, + event); + } + + template<class T> + bool isStateEvent(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative<StateEvent<Aliases>>(e) || + std::holds_alternative<StateEvent<state::Avatar>>(e) || + std::holds_alternative<StateEvent<CanonicalAlias>>(e) || + std::holds_alternative<StateEvent<Create>>(e) || + std::holds_alternative<StateEvent<GuestAccess>>(e) || + std::holds_alternative<StateEvent<HistoryVisibility>>(e) || + std::holds_alternative<StateEvent<JoinRules>>(e) || + std::holds_alternative<StateEvent<Name>>(e) || + std::holds_alternative<StateEvent<Member>>(e) || + std::holds_alternative<StateEvent<PowerLevels>>(e) || + std::holds_alternative<StateEvent<Topic>>(e); + } + + template<class T> + bool containsStateUpdates(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative<StateEvent<state::Avatar>>(e) || + std::holds_alternative<StateEvent<CanonicalAlias>>(e) || + std::holds_alternative<StateEvent<Name>>(e) || + std::holds_alternative<StateEvent<Member>>(e) || + std::holds_alternative<StateEvent<Topic>>(e); + } + + bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative<StrippedEvent<state::Avatar>>(e) || + std::holds_alternative<StrippedEvent<CanonicalAlias>>(e) || + std::holds_alternative<StrippedEvent<Name>>(e) || + std::holds_alternative<StrippedEvent<Member>>(e) || + std::holds_alternative<StrippedEvent<Topic>>(e); + } + + void saveInvites(lmdb::txn &txn, + const std::map<std::string, mtx::responses::InvitedRoom> &rooms); + + //! Sends signals for the rooms that are removed. + void removeLeftRooms(lmdb::txn &txn, + const std::map<std::string, mtx::responses::LeftRoom> &rooms) + { + for (const auto &room : rooms) { + removeRoom(txn, room.first); + + // Clean up leftover invites. + removeInvite(txn, room.first); + } + } + + lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); + } + + lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) + { + auto db = + lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); + lmdb::dbi_set_compare(txn, db, numeric_key_comparison); + + return db; + } + + lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); + } + + lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); + } + + lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); + } + + lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); + } + + lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); + } + + //! Retrieves or creates the database that stores the open OLM sessions between our device + //! and the given curve25519 key which represents another device. + //! + //! Each entry is a map from the session_id to the pickled representation of the session. + lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) + { + return lmdb::dbi::open( + txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); + } + + QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) + { + if (!event.content.display_name.empty()) + return QString::fromStdString(event.content.display_name); + + return QString::fromStdString(event.state_key); + } + + void setNextBatchToken(lmdb::txn &txn, const std::string &token); + void setNextBatchToken(lmdb::txn &txn, const QString &token); + + lmdb::env env_; + lmdb::dbi syncStateDb_; + lmdb::dbi roomsDb_; + lmdb::dbi invitesDb_; + lmdb::dbi mediaDb_; + lmdb::dbi readReceiptsDb_; + lmdb::dbi notificationsDb_; + + lmdb::dbi devicesDb_; + lmdb::dbi deviceKeysDb_; + + lmdb::dbi inboundMegolmSessionDb_; + lmdb::dbi outboundMegolmSessionDb_; + + QString localUserId_; + QString cacheDirectory_; + + static QHash<QString, QString> DisplayNames; + static QHash<QString, QString> AvatarUrls; + + OlmSessionStorage session_storage; +}; + +namespace cache { +Cache * +client(); +} diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index dd23fb80..89bfd55a 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -18,10 +18,12 @@ #include <QApplication> #include <QImageReader> #include <QSettings> +#include <QShortcut> #include <QtConcurrent> #include "AvatarProvider.h" #include "Cache.h" +#include "Cache_p.h" #include "ChatPage.h" #include "Logging.h" #include "MainWindow.h" @@ -33,7 +35,6 @@ #include "Splitter.h" #include "TextInputWidget.h" #include "TopRoomBar.h" -#include "TypingDisplay.h" #include "UserInfoWidget.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -43,6 +44,7 @@ #include "notifications/Manager.h" #include "dialogs/ReadReceipts.h" +#include "popups/UserMentions.h" #include "timeline/TimelineViewManager.h" // TODO: Needs to be updated with an actual secret. @@ -53,6 +55,9 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; constexpr int RETRY_TIMEOUT = 5'000; constexpr size_t MAX_ONETIME_KEYS = 50; +Q_DECLARE_METATYPE(std::optional<mtx::crypto::EncryptedFile>) +Q_DECLARE_METATYPE(std::optional<RelatedInfo>) + ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) : QWidget(parent) , isConnected_(true) @@ -61,6 +66,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) { setObjectName("chatPage"); + qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>(); + qRegisterMetaType<std::optional<RelatedInfo>>(); + topLayout_ = new QHBoxLayout(this); topLayout_->setSpacing(0); topLayout_->setMargin(0); @@ -76,7 +84,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) // SideBar sideBar_ = new QFrame(this); sideBar_->setObjectName("sideBar"); - sideBar_->setMinimumWidth(utils::calculateSidebarSizes(QFont{}).normal); + sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal); sideBarLayout_ = new QVBoxLayout(sideBar_); sideBarLayout_->setSpacing(0); sideBarLayout_->setMargin(0); @@ -88,8 +96,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); - user_info_widget_ = new UserInfoWidget(sideBar_); - room_list_ = new RoomList(sideBar_); + user_info_widget_ = new UserInfoWidget(sideBar_); + user_mentions_popup_ = new popups::UserMentions(); + room_list_ = new RoomList(sideBar_); connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); sideBarLayout_->addWidget(user_info_widget_); @@ -108,15 +117,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) contentLayout_->setMargin(0); top_bar_ = new TopRoomBar(this); - view_manager_ = new TimelineViewManager(this); + view_manager_ = new TimelineViewManager(userSettings_, this); contentLayout_->addWidget(top_bar_); - contentLayout_->addWidget(view_manager_); - - connect(this, - &ChatPage::removeTimelineEvent, - view_manager_, - &TimelineViewManager::removeTimelineEvent); + contentLayout_->addWidget(view_manager_->getWidget()); // Splitter splitter->addWidget(sideBar_); @@ -126,11 +130,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) text_input_ = new TextInputWidget(this); contentLayout_->addWidget(text_input_); - typingDisplay_ = new TypingDisplay(content_); - typingDisplay_->hide(); - connect( - text_input_, &TextInputWidget::heightChanged, typingDisplay_, &TypingDisplay::setOffset); - typingRefresher_ = new QTimer(this); typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); @@ -150,6 +149,41 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) trySync(); }); + connect( + new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() { + if (isVisible()) + room_list_->nextRoom(); + }); + connect( + new QShortcut(QKeySequence("Ctrl+Up"), this), &QShortcut::activated, this, [this]() { + if (isVisible()) + room_list_->previousRoom(); + }); + + connect(top_bar_, &TopRoomBar::mentionsClicked, this, [this](const QPoint &mentionsPos) { + if (user_mentions_popup_->isVisible()) { + user_mentions_popup_->hide(); + } else { + showNotificationsDialog(mentionsPos); + http::client()->notifications( + 1000, + "", + "highlight", + [this, mentionsPos](const mtx::responses::Notifications &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to retrieve notifications: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit highlightedNotifsRetrieved(std::move(res), mentionsPos); + }); + } + }); + connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { if (http::client()->access_token().empty()) { @@ -186,30 +220,16 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) mtx::http::RequestErr err) { if (err) { emit showNotification( - QString("Failed to invite user: %1").arg(user)); + tr("Failed to invite user: %1").arg(user)); return; } - emit showNotification( - QString("Invited user: %1").arg(user)); + emit showNotification(tr("Invited user: %1").arg(user)); }); }); } }); - connect(room_list_, &RoomList::roomChanged, this, [this](const QString &roomid) { - QStringList users; - - if (!userSettings_->isTypingNotificationsEnabled()) { - typingDisplay_->setUsers(users); - return; - } - - if (typingUsers_.find(roomid) != typingUsers_.end()) - users = typingUsers_[roomid]; - - typingDisplay_->setUsers(users); - }); connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping); connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo); connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); @@ -260,22 +280,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) SLOT(showUnreadMessageNotification(int))); connect(text_input_, - SIGNAL(sendTextMessage(const QString &)), + &TextInputWidget::sendTextMessage, view_manager_, - SLOT(queueTextMessage(const QString &))); + &TimelineViewManager::queueTextMessage); connect(text_input_, - SIGNAL(sendEmoteMessage(const QString &)), + &TextInputWidget::sendEmoteMessage, view_manager_, - SLOT(queueEmoteMessage(const QString &))); + &TimelineViewManager::queueEmoteMessage); connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); + // invites and bans via quick command + connect(text_input_, &TextInputWidget::sendInviteRoomRequest, this, &ChatPage::inviteUser); + connect(text_input_, &TextInputWidget::sendKickRoomRequest, this, &ChatPage::kickUser); + connect(text_input_, &TextInputWidget::sendBanRoomRequest, this, &ChatPage::banUser); + connect(text_input_, &TextInputWidget::sendUnbanRoomRequest, this, &ChatPage::unbanUser); + connect( text_input_, - &TextInputWidget::uploadImage, + &TextInputWidget::uploadMedia, this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { + [this](QSharedPointer<QIODevice> dev, + QString mimeClass, + const QString &fn, + const std::optional<RelatedInfo> &related) { QMimeDatabase db; QMimeType mime = db.mimeTypeForData(dev.data()); @@ -285,205 +314,89 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) return; } - auto bin = dev->peek(dev->size()); - auto payload = std::string(bin.data(), bin.size()); - auto dimensions = QImageReader(dev.data()).size(); + auto bin = dev->peek(dev->size()); + auto payload = std::string(bin.data(), bin.size()); + std::optional<mtx::crypto::EncryptedFile> encryptedFile; + if (cache::isRoomEncrypted(current_room_.toStdString())) { + mtx::crypto::BinaryBuf buf; + std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); + payload = mtx::crypto::to_string(buf); + } + + QSize dimensions; + if (mimeClass == "image") + dimensions = QImageReader(dev.data()).size(); http::client()->upload( payload, - mime.name().toStdString(), + encryptedFile ? "application/octet-stream" : mime.name().toStdString(), QFileInfo(fn).fileName().toStdString(), [this, room_id = current_room_, filename = fn, - mime = mime.name(), - size = payload.size(), - dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { + encryptedFile, + mimeClass, + mime = mime.name(), + size = payload.size(), + dimensions, + related](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { if (err) { emit uploadFailed( - tr("Failed to upload image. Please try again.")); - nhlog::net()->warn("failed to upload image: {} {} ({})", + tr("Failed to upload media. Please try again.")); + nhlog::net()->warn("failed to upload media: {} {} ({})", err->matrix_error.error, to_string(err->matrix_error.errcode), static_cast<int>(err->status_code)); return; } - emit imageUploaded(room_id, + emit mediaUploaded(room_id, filename, + encryptedFile, QString::fromStdString(res.content_uri), + mimeClass, mime, size, - dimensions); + dimensions, + related); }); }); - connect(text_input_, - &TextInputWidget::uploadFile, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload file. Please try again.")); - nhlog::net()->warn("failed to upload file: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit fileUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - - connect(text_input_, - &TextInputWidget::uploadAudio, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload audio. Please try again.")); - nhlog::net()->warn("failed to upload audio: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit audioUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(text_input_, - &TextInputWidget::uploadVideo, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload video. Please try again.")); - nhlog::net()->warn("failed to upload video: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit videoUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { text_input_->hideUploadSpinner(); emit showNotification(msg); }); - connect(this, - &ChatPage::imageUploaded, - this, - [this](QString roomid, - QString filename, - QString url, - QString mime, - qint64 dsize, - QSize dimensions) { - text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage( - roomid, filename, url, mime, dsize, dimensions); - }); - connect(this, - &ChatPage::fileUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::audioUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::videoUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); - }); + connect( + this, + &ChatPage::mediaUploaded, + this, + [this](QString roomid, + QString filename, + std::optional<mtx::crypto::EncryptedFile> encryptedFile, + QString url, + QString mimeClass, + QString mime, + qint64 dsize, + QSize dimensions, + const std::optional<RelatedInfo> &related) { + text_input_->hideUploadSpinner(); + + if (encryptedFile) + encryptedFile->url = url.toStdString(); + + if (mimeClass == "image") + view_manager_->queueImageMessage( + roomid, filename, encryptedFile, url, mime, dsize, dimensions, related); + else if (mimeClass == "audio") + view_manager_->queueAudioMessage( + roomid, filename, encryptedFile, url, mime, dsize, related); + else if (mimeClass == "video") + view_manager_->queueVideoMessage( + roomid, filename, encryptedFile, url, mime, dsize, related); + else + view_manager_->queueFileMessage( + roomid, filename, encryptedFile, url, mime, dsize, related); + }); connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); @@ -492,6 +405,16 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications); + connect(this, + &ChatPage::highlightedNotifsRetrieved, + this, + [](const mtx::responses::Notifications ¬if) { + try { + cache::saveTimelineMentions(notif); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to save mentions: {}", e.what()); + } + }); connect(communitiesList_, &CommunitiesList::communityChanged, @@ -525,26 +448,28 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::initializeViews, view_manager_, - [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); + [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); }); connect(this, &ChatPage::initializeEmptyViews, view_manager_, &TimelineViewManager::initWithMessages); + connect(this, + &ChatPage::initializeMentions, + user_mentions_popup_, + &popups::UserMentions::initializeMentions); connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { try { - room_list_->cleanupInvites(cache::client()->invites()); + room_list_->cleanupInvites(cache::invites()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to retrieve invites: {}", e.what()); } - view_manager_->initialize(rooms); + view_manager_->sync(rooms); removeLeftRooms(rooms.leave); bool hasNotifications = false; for (const auto &room : rooms.join) { auto room_id = QString::fromStdString(room.first); - - updateTypingUsers(room_id, room.second.ephemeral.typing); updateRoomNotificationCount( room_id, room.second.unread_notifications.notification_count, @@ -557,6 +482,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) if (hasNotifications && userSettings_->hasDesktopNotifications()) http::client()->notifications( 5, + "", + "", [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) { if (err) { @@ -594,6 +521,13 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); connect(this, &ChatPage::messageReply, text_input_, &TextInputWidget::addReply); + connect(this, &ChatPage::messageReply, this, [this](const RelatedInfo &related) { + view_manager_->updateReplyingEvent(QString::fromStdString(related.related_event)); + }); + connect(view_manager_, + &TimelineViewManager::replyClosed, + text_input_, + &TextInputWidget::closeReplyPopup); instance_ = this; } @@ -648,7 +582,7 @@ ChatPage::deleteConfigs() settings.remove(""); settings.endGroup(); - cache::client()->deleteData(); + cache::deleteData(); http::client()->clear(); } @@ -683,18 +617,18 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) connect( cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus); - const bool isInitialized = cache::client()->isInitialized(); - const bool isValid = cache::client()->isFormatValid(); + const bool isInitialized = cache::isInitialized(); + const bool isValid = cache::isFormatValid(); if (!isInitialized) { - cache::client()->setCurrentFormat(); + cache::setCurrentFormat(); } else if (isInitialized && !isValid) { // TODO: Deleting session data but keep using the // same device doesn't work. - cache::client()->deleteData(); + cache::deleteData(); cache::init(userid); - cache::client()->setCurrentFormat(); + cache::setCurrentFormat(); } else if (isInitialized) { loadStateFromCache(); return; @@ -702,7 +636,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } catch (const lmdb::error &e) { nhlog::db()->critical("failure during boot: {}", e.what()); - cache::client()->deleteData(); + cache::deleteData(); nhlog::net()->info("falling back to initial sync"); } @@ -711,7 +645,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) // There isn't a saved olm account to restore. nhlog::crypto()->info("creating new olm account"); olm::client()->create_new_account(); - cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save olm account {}", e.what()); emit dropToLoginPageCb(QString::fromStdString(e.what())); @@ -727,12 +661,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } void -ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) +ChatPage::updateTopBarAvatar(const QString &roomid, const QString &img) { if (current_room_ != roomid) return; - top_bar_->updateRoomAvatar(img.toImage()); + top_bar_->updateRoomAvatar(img); } void @@ -744,7 +678,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) } try { - auto room_info = cache::client()->getRoomInfo({room_id.toStdString()}); + auto room_info = cache::getRoomInfo({room_id.toStdString()}); if (room_info.find(room_id) == room_info.end()) return; @@ -755,12 +689,12 @@ ChatPage::changeTopRoomInfo(const QString &room_id) top_bar_->updateRoomName(name); top_bar_->updateRoomTopic(QString::fromStdString(room_info[room_id].topic)); - auto img = cache::client()->getRoomAvatar(room_id); + auto img = cache::getRoomAvatar(room_id); if (img.isNull()) top_bar_->updateRoomAvatarFromName(name); else - top_bar_->updateRoomAvatar(img); + top_bar_->updateRoomAvatar(avatar_url); } catch (const lmdb::error &e) { nhlog::ui()->error("failed to change top bar room info: {}", e.what()); @@ -792,17 +726,17 @@ ChatPage::loadStateFromCache() QtConcurrent::run([this]() { try { - cache::client()->restoreSessions(); - olm::client()->load(cache::client()->restoreOlmAccount(), - STORAGE_SECRET_KEY); + cache::restoreSessions(); + olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - cache::client()->populateMembers(); + cache::populateMembers(); - emit initializeEmptyViews(cache::client()->roomMessages()); - emit initializeRoomList(cache::client()->roomInfo()); - emit syncTags(cache::client()->roomInfo().toStdMap()); + emit initializeEmptyViews(cache::roomMessages()); + emit initializeRoomList(cache::roomInfo()); + emit initializeMentions(cache::getTimelineMentions()); + emit syncTags(cache::roomInfo().toStdMap()); - cache::client()->calculateRoomReadStatus(); + cache::calculateRoomReadStatus(); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); @@ -845,8 +779,8 @@ void ChatPage::removeRoom(const QString &room_id) { try { - cache::client()->removeRoom(room_id); - cache::client()->removeInvite(room_id.toStdString()); + cache::removeRoom(room_id); + cache::removeInvite(room_id.toStdString()); } catch (const lmdb::error &e) { nhlog::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. @@ -856,38 +790,6 @@ ChatPage::removeRoom(const QString &room_id) } void -ChatPage::updateTypingUsers(const QString &roomid, const std::vector<std::string> &user_ids) -{ - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - typingUsers_[roomid] = generateTypingUsers(roomid, user_ids); - - if (current_room_ == roomid) - typingDisplay_->setUsers(typingUsers_[roomid]); -} - -QStringList -ChatPage::generateTypingUsers(const QString &room_id, const std::vector<std::string> &typing_users) -{ - QStringList users; - auto local_user = utils::localUser(); - - for (const auto &uid : typing_users) { - const auto remote_user = QString::fromStdString(uid); - - if (remote_user == local_user) - continue; - - users.append(Cache::displayName(room_id, remote_user)); - } - - users.sort(); - - return users; -} - -void ChatPage::removeLeftRooms(const std::map<std::string, mtx::responses::LeftRoom> &rooms) { for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { @@ -925,16 +827,16 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) try { if (item.read) { - cache::client()->removeReadNotification(event_id); + cache::removeReadNotification(event_id); continue; } - if (!cache::client()->isNotificationSent(event_id)) { + if (!cache::isNotificationSent(event_id)) { const auto room_id = QString::fromStdString(item.room_id); const auto user_id = utils::event_sender(item.event); // We should only sent one notification per event. - cache::client()->markSentNotification(event_id); + cache::markSentNotification(event_id); // Don't send a notification when the current room is opened. if (isRoomActive(room_id)) @@ -943,11 +845,10 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) notificationsManager.postNotification( room_id, QString::fromStdString(event_id), - QString::fromStdString( - cache::client()->singleRoomInfo(item.room_id).name), - Cache::displayName(room_id, user_id), + QString::fromStdString(cache::singleRoomInfo(item.room_id).name), + cache::displayName(room_id, user_id), utils::event_body(item.event), - cache::client()->getRoomAvatar(room_id)); + cache::getRoomAvatar(room_id)); } } catch (const lmdb::error &e) { nhlog::db()->warn("error while sending desktop notification: {}", e.what()); @@ -956,6 +857,18 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) } void +ChatPage::showNotificationsDialog(const QPoint &widgetPos) +{ + auto notifDialog = user_mentions_popup_; + + notifDialog->setGeometry( + widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2); + + notifDialog->raise(); + notifDialog->showPopup(); +} + +void ChatPage::tryInitialSync() { nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); @@ -1022,7 +935,7 @@ ChatPage::trySync() connectivityTimer_.start(); try { - opts.since = cache::client()->nextBatchToken(); + opts.since = cache::nextBatchToken(); } catch (const lmdb::error &e) { nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); return; @@ -1075,22 +988,22 @@ ChatPage::trySync() // TODO: fine grained error handling try { - cache::client()->saveState(res); + cache::saveState(res); olm::handle_to_device_messages(res.to_device); emit syncUI(res.rooms); - auto updates = cache::client()->roomUpdates(res); + auto updates = cache::roomUpdates(res); emit syncTopBar(updates); emit syncRoomlist(updates); - emit syncTags(cache::client()->roomTagUpdates(res)); + emit syncTags(cache::roomTagUpdates(res)); - cache::client()->deleteOldData(); + cache::deleteOldData(); } catch (const lmdb::map_full_error &e) { nhlog::db()->error("lmdb is full: {}", e.what()); - cache::client()->deleteOldData(); + cache::deleteOldData(); } catch (const lmdb::error &e) { nhlog::db()->error("saving sync response: {}", e.what()); } @@ -1109,19 +1022,18 @@ ChatPage::joinRoom(const QString &room) room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) { if (err) { emit showNotification( - QString("Failed to join room: %1") + tr("Failed to join room: %1") .arg(QString::fromStdString(err->matrix_error.error))); return; } - emit showNotification("You joined the room"); + emit tr("You joined the room"); // We remove any invites with the same room_id. try { - cache::client()->removeInvite(room_id); + cache::removeInvite(room_id); } catch (const lmdb::error &e) { - emit showNotification( - QString("Failed to remove invite: %1").arg(e.what())); + emit showNotification(tr("Failed to remove invite: %1").arg(e.what())); } }); } @@ -1144,8 +1056,8 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req) return; } - emit showNotification(QString("Room %1 created") - .arg(QString::fromStdString(res.room_id.to_string()))); + emit showNotification( + tr("Room %1 created").arg(QString::fromStdString(res.room_id.to_string()))); }); } @@ -1166,6 +1078,83 @@ ChatPage::leaveRoom(const QString &room_id) } void +ChatPage::inviteUser(QString userid, QString reason) +{ + http::client()->invite_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to invite %1 to %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Invited user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} +void +ChatPage::kickUser(QString userid, QString reason) +{ + http::client()->kick_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to kick %1 to %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Kicked user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} +void +ChatPage::banUser(QString userid, QString reason) +{ + http::client()->ban_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to ban %1 in %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Banned user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} +void +ChatPage::unbanUser(QString userid, QString reason) +{ + http::client()->unban_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to unban %1 in %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Unbanned user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} + +void ChatPage::sendTypingNotifications() { if (!userSettings_->isTypingNotificationsEnabled()) @@ -1183,6 +1172,8 @@ ChatPage::sendTypingNotifications() void ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) { + // TODO: Initial Sync should include mentions as well... + if (err) { const auto error = QString::fromStdString(err->matrix_error.error); const auto msg = tr("Please try to login again: %1").arg(error); @@ -1214,15 +1205,16 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request nhlog::net()->info("initial sync completed"); try { - cache::client()->saveState(res); + cache::saveState(res); olm::handle_to_device_messages(res.to_device); emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::client()->roomInfo()); + emit initializeRoomList(cache::roomInfo()); + emit initializeMentions(cache::getTimelineMentions()); - cache::client()->calculateRoomReadStatus(); - emit syncTags(cache::client()->roomInfo().toStdMap()); + cache::calculateRoomReadStatus(); + emit syncTags(cache::roomInfo().toStdMap()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to save state after initial sync: {}", e.what()); startInitialSync(); @@ -1274,37 +1266,7 @@ ChatPage::getProfileInfo() emit setUserDisplayName(QString::fromStdString(res.display_name)); - if (cache::client()) { - auto data = cache::client()->image(res.avatar_url); - if (!data.isNull()) { - emit setUserAvatar(QImage::fromData(data)); - return; - } - } - - if (res.avatar_url.empty()) - return; - - http::client()->download( - res.avatar_url, - [this, res](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download user avatar: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(res.avatar_url, data); - - emit setUserAvatar( - QImage::fromData(QByteArray(data.data(), data.size()))); - }); + emit setUserAvatar(QString::fromStdString(res.avatar_url)); }); http::client()->joined_groups( @@ -1349,7 +1311,7 @@ ChatPage::timelineWidth() bool ChatPage::isSideBarExpanded() { - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); return sideBar_->size().width() > sz.normal; } diff --git a/src/ChatPage.h b/src/ChatPage.h index 7d3b3273..8e2e9192 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -18,19 +18,27 @@ #pragma once #include <atomic> -#include <boost/variant.hpp> +#include <optional> +#include <variant> + +#include <mtx/common.hpp> +#include <mtx/requests.hpp> +#include <mtx/responses.hpp> +#include <mtxclient/http/errors.hpp> #include <QFrame> #include <QHBoxLayout> #include <QMap> #include <QPixmap> +#include <QPoint> #include <QTimer> #include <QWidget> -#include "Cache.h" +#include "CacheStructs.h" #include "CommunitiesList.h" -#include "MatrixClient.h" +#include "Utils.h" #include "notifications/Manager.h" +#include "popups/UserMentions.h" class OverlayModal; class QuickSwitcher; @@ -40,7 +48,6 @@ class Splitter; class TextInputWidget; class TimelineViewManager; class TopRoomBar; -class TypingDisplay; class UserInfoWidget; class UserSettings; class NotificationsManager; @@ -49,12 +56,16 @@ constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int SHOW_CONTENT_TIMEOUT = 3000; constexpr int TYPING_REFRESH_TIMEOUT = 10000; +namespace mtx::http { +using RequestErr = const std::optional<mtx::http::ClientError> &; +} + class ChatPage : public QWidget { Q_OBJECT public: - ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = 0); + ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); // Initialize all the components of the UI. void bootstrap(QString userid, QString homeserver, QString token); @@ -79,36 +90,31 @@ public slots: void leaveRoom(const QString &room_id); void createRoom(const mtx::requests::CreateRoom &req); + void inviteUser(QString userid, QString reason); + void kickUser(QString userid, QString reason); + void banUser(QString userid, QString reason); + void unbanUser(QString userid, QString reason); + signals: void connectionLost(); void connectionRestored(); - void messageReply(const QString &username, const QString &msg); + void messageReply(const RelatedInfo &related); void notificationsRetrieved(const mtx::responses::Notifications &); + void highlightedNotifsRetrieved(const mtx::responses::Notifications &, + const QPoint widgetPos); void uploadFailed(const QString &msg); - void imageUploaded(const QString &roomid, + void mediaUploaded(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, + const QString &mimeClass, const QString &mime, qint64 dsize, - const QSize &dimensions); - void fileUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void audioUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void videoUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); + const QSize &dimensions, + const std::optional<RelatedInfo> &related); void contentLoaded(); void closing(); @@ -119,11 +125,9 @@ signals: void showUserSettingsPage(); void showOverlayProgressBar(); - void removeTimelineEvent(const QString &room_id, const QString &event_id); - void ownProfileOk(); void setUserDisplayName(const QString &name); - void setUserAvatar(const QImage &avatar); + void setUserAvatar(const QString &avatar); void loggedOut(); void trySyncCb(); @@ -134,6 +138,7 @@ signals: void initializeRoomList(QMap<QString, RoomInfo>); void initializeViews(const mtx::responses::Rooms &rooms); void initializeEmptyViews(const std::map<QString, mtx::responses::Timeline> &msgs); + void initializeMentions(const QMap<QString, mtx::responses::Notifications> ¬ifs); void syncUI(const mtx::responses::Rooms &rooms); void syncRoomlist(const std::map<QString, RoomInfo> &updates); void syncTags(const std::map<QString, RoomInfo> &updates); @@ -152,7 +157,7 @@ signals: private slots: void showUnreadMessageNotification(int count); - void updateTopBarAvatar(const QString &roomid, const QPixmap &img); + void updateTopBarAvatar(const QString &roomid, const QString &img); void changeTopRoomInfo(const QString &room_id); void logout(); void removeRoom(const QString &room_id); @@ -186,8 +191,6 @@ private: using LeftRooms = std::map<std::string, mtx::responses::LeftRoom>; void removeLeftRooms(const LeftRooms &rooms); - void updateTypingUsers(const QString &roomid, const std::vector<std::string> &user_ids); - void loadStateFromCache(); void resetUI(); //! Decides whether or not to hide the group's sidebar. @@ -203,8 +206,7 @@ private: //! Send desktop notification for the received messages. void sendDesktopNotifications(const mtx::responses::Notifications &); - QStringList generateTypingUsers(const QString &room_id, - const std::vector<std::string> &typing_users); + void showNotificationsDialog(const QPoint &point); QHBoxLayout *topLayout_; Splitter *splitter; @@ -225,7 +227,6 @@ private: TopRoomBar *top_bar_; TextInputWidget *text_input_; - TypingDisplay *typingDisplay_; QTimer connectivityTimer_; std::atomic_bool isConnected_; @@ -235,8 +236,8 @@ private: UserInfoWidget *user_info_widget_; - // Keeps track of the users currently typing on each room. - std::map<QString, QList<QString>> typingUsers_; + popups::UserMentions *user_mentions_popup_; + QTimer *typingRefresher_; // Global user settings. @@ -254,9 +255,8 @@ ChatPage::getMemberships(const std::vector<Collection> &collection) const using Member = mtx::events::StateEvent<mtx::events::state::Member>; for (const auto &event : collection) { - if (boost::get<Member>(event) != nullptr) { - auto member = boost::get<Member>(event); - memberships.emplace(member.state_key, member); + if (auto member = std::get_if<Member>(event)) { + memberships.emplace(member->state_key, *member); } } diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp new file mode 100644 index 00000000..c580c394 --- /dev/null +++ b/src/ColorImageProvider.cpp @@ -0,0 +1,27 @@ +#include "ColorImageProvider.h" + +#include <QPainter> + +QPixmap +ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &) +{ + auto args = id.split('?'); + + QPixmap source(args[0]); + + if (size) + *size = QSize(source.width(), source.height()); + + if (args.size() < 2) + return source; + + QColor color(args[1]); + + QPixmap colorized = source; + QPainter painter(&colorized); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(colorized.rect(), color); + painter.end(); + + return colorized; +} diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h new file mode 100644 index 00000000..21f36c12 --- /dev/null +++ b/src/ColorImageProvider.h @@ -0,0 +1,11 @@ +#include <QQuickImageProvider> + +class ColorImageProvider : public QQuickImageProvider +{ +public: + ColorImageProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) + {} + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; +}; diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp index 6e46741b..bb57ca40 100644 --- a/src/CommunitiesList.cpp +++ b/src/CommunitiesList.cpp @@ -2,7 +2,9 @@ #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" -#include "Utils.h" +#include "Splitter.h" + +#include <mtx/responses/groups.hpp> #include <QLabel> @@ -14,13 +16,11 @@ CommunitiesList::CommunitiesList(QWidget *parent) sizePolicy.setVerticalStretch(1); setSizePolicy(sizePolicy); - setStyleSheet("border-style: none;"); - topLayout_ = new QVBoxLayout(this); topLayout_->setSpacing(0); topLayout_->setMargin(0); - const auto sideBarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{}); setFixedWidth(sideBarSizes.groups); scrollArea_ = new QScrollArea(this); @@ -30,16 +30,14 @@ CommunitiesList::CommunitiesList(QWidget *parent) scrollArea_->setWidgetResizable(true); scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); - scrollAreaContents_ = new QWidget(); - - contentsLayout_ = new QVBoxLayout(scrollAreaContents_); + contentsLayout_ = new QVBoxLayout(); contentsLayout_->setSpacing(0); contentsLayout_->setMargin(0); addGlobalItem(); contentsLayout_->addStretch(1); - scrollArea_->setWidget(scrollAreaContents_); + scrollArea_->setLayout(contentsLayout_); topLayout_->addWidget(scrollArea_); connect( @@ -185,7 +183,8 @@ void CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img) { if (!communityExists(community_id)) { - qWarning() << "Avatar update on nonexistent community" << community_id; + nhlog::ui()->warn("Avatar update on nonexistent community {}", + community_id.toStdString()); return; } @@ -196,7 +195,7 @@ void CommunitiesList::highlightSelectedCommunity(const QString &community_id) { if (!communityExists(community_id)) { - qDebug() << "CommunitiesList: clicked unknown community"; + nhlog::ui()->debug("CommunitiesList: clicked unknown community"); return; } @@ -215,7 +214,7 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id) void CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) { - auto savedImgData = cache::client()->image(avatarUrl); + auto savedImgData = cache::image(avatarUrl); if (!savedImgData.isNull()) { QPixmap pix; pix.loadFromData(savedImgData); @@ -238,7 +237,7 @@ CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUr return; } - cache::client()->saveImage(opts.mxc_url, res); + cache::saveImage(opts.mxc_url, res); auto data = QByteArray(res.data(), res.size()); diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h index b18df654..d3cbeeff 100644 --- a/src/CommunitiesList.h +++ b/src/CommunitiesList.h @@ -4,10 +4,15 @@ #include <QSharedPointer> #include <QVBoxLayout> -#include "Cache.h" +#include "CacheStructs.h" #include "CommunitiesListItem.h" #include "ui/Theme.h" +namespace mtx::responses { +struct GroupProfile; +struct JoinedGroups; +} + class CommunitiesList : public QWidget { Q_OBJECT @@ -48,7 +53,6 @@ private: QVBoxLayout *topLayout_; QVBoxLayout *contentsLayout_; - QWidget *scrollAreaContents_; QScrollArea *scrollArea_; std::map<QString, QSharedPointer<CommunitiesListItem>> communities_; diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp index 324482d3..274271e5 100644 --- a/src/CommunitiesListItem.cpp +++ b/src/CommunitiesListItem.cpp @@ -1,4 +1,7 @@ #include "CommunitiesListItem.h" + +#include <QMouseEvent> + #include "Utils.h" #include "ui/Painter.h" #include "ui/Ripple.h" diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h index d4d7e9c6..0cc5d60c 100644 --- a/src/CommunitiesListItem.h +++ b/src/CommunitiesListItem.h @@ -1,17 +1,14 @@ #pragma once -#include <QDebug> -#include <QMouseEvent> -#include <QPainter> #include <QSharedPointer> #include <QWidget> -#include <mtx/responses/groups.hpp> - #include "Config.h" #include "ui/Theme.h" class RippleOverlay; +class QPainter; +class QMouseEvent; class CommunitiesListItem : public QWidget { diff --git a/src/Config.h b/src/Config.h index e1271452..f99cf36b 100644 --- a/src/Config.h +++ b/src/Config.h @@ -53,9 +53,9 @@ namespace strings { const QString url_html = "<a href=\"\\1\">\\1</a>"; const QRegularExpression url_regex( // match an URL, that is not quoted, i.e. - // vvvvvvv match quote via negative lookahead/lookbehind vvvvvv - // vvvv atomic match url -> fail if there is a " before or after vv - "(?<!\")(?>((www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]\\)\\:]))(?!\")"); + // vvvvvv match quote via negative lookahead/lookbehind vv + // vvvv atomic match url -> fail if there is a " before or after vvv + R"((?<!")(?>((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!"))"); } // Window geometry. diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp new file mode 100644 index 00000000..20cdb63c --- /dev/null +++ b/src/EventAccessors.cpp @@ -0,0 +1,383 @@ +#include "EventAccessors.h" + +#include <type_traits> + +namespace { +struct nonesuch +{ + ~nonesuch() = delete; + nonesuch(nonesuch const &) = delete; + void operator=(nonesuch const &) = delete; +}; + +namespace detail { +template<class Default, class AlwaysVoid, template<class...> class Op, class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template<class Default, template<class...> class Op, class... Args> +struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op<Args...>; +}; + +} // namespace detail + +template<template<class...> class Op, class... Args> +using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t; + +struct EventMsgType +{ + template<class E> + using msgtype_t = decltype(E::msgtype); + template<class T> + mtx::events::MessageType operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<msgtype_t, T>::value) + return mtx::events::getMessageType(e.content.msgtype); + return mtx::events::MessageType::Unknown; + } +}; + +struct EventRoomName +{ + template<class T> + std::string operator()(const T &e) + { + if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Name>, T>) + return e.content.name; + return ""; + } +}; + +struct EventRoomTopic +{ + template<class T> + std::string operator()(const T &e) + { + if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Topic>, T>) + return e.content.topic; + return ""; + } +}; + +struct EventBody +{ + template<class C> + using body_t = decltype(C::body); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<body_t, T>::value) + return e.content.body; + return ""; + } +}; + +struct EventFormattedBody +{ + template<class C> + using formatted_body_t = decltype(C::formatted_body); + template<class T> + std::string operator()(const mtx::events::RoomEvent<T> &e) + { + if constexpr (is_detected<formatted_body_t, T>::value) + return e.content.formatted_body; + return ""; + } +}; + +struct EventFile +{ + template<class Content> + using file_t = decltype(Content::file); + template<class T> + std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<file_t, T>::value) + return e.content.file; + return std::nullopt; + } +}; + +struct EventUrl +{ + template<class Content> + using url_t = decltype(Content::url); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<url_t, T>::value) { + if (auto file = EventFile{}(e)) + return file->url; + return e.content.url; + } + return ""; + } +}; + +struct EventThumbnailUrl +{ + template<class Content> + using thumbnail_url_t = decltype(Content::info.thumbnail_url); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<thumbnail_url_t, T>::value) { + return e.content.info.thumbnail_url; + } + return ""; + } +}; + +struct EventFilename +{ + template<class T> + std::string operator()(const mtx::events::Event<T> &) + { + return ""; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Video> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Image> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::File> &e) + { + // body may be the original filename + if (!e.content.filename.empty()) + return e.content.filename; + return e.content.body; + } +}; + +struct EventMimeType +{ + template<class Content> + using mimetype_t = decltype(Content::info.mimetype); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<mimetype_t, T>::value) { + return e.content.info.mimetype; + } + return ""; + } +}; + +struct EventFilesize +{ + template<class Content> + using filesize_t = decltype(Content::info.size); + template<class T> + int64_t operator()(const mtx::events::RoomEvent<T> &e) + { + if constexpr (is_detected<filesize_t, T>::value) { + return e.content.info.size; + } + return 0; + } +}; + +struct EventInReplyTo +{ + template<class Content> + using related_ev_id_t = decltype(Content::relates_to.in_reply_to.event_id); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<related_ev_id_t, T>::value) { + return e.content.relates_to.in_reply_to.event_id; + } + return ""; + } +}; + +struct EventTransactionId +{ + template<class T> + std::string operator()(const mtx::events::RoomEvent<T> &e) + { + return e.unsigned_data.transaction_id; + } + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + return e.unsigned_data.transaction_id; + } +}; + +struct EventMediaHeight +{ + template<class Content> + using h_t = decltype(Content::info.h); + template<class T> + uint64_t operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<h_t, T>::value) { + return e.content.info.h; + } + return -1; + } +}; + +struct EventMediaWidth +{ + template<class Content> + using w_t = decltype(Content::info.w); + template<class T> + uint64_t operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<w_t, T>::value) { + return e.content.info.w; + } + return -1; + } +}; + +template<class T> +double +eventPropHeight(const mtx::events::RoomEvent<T> &e) +{ + auto w = eventWidth(e); + if (w == 0) + w = 1; + + double prop = eventHeight(e) / (double)w; + + return prop > 0 ? prop : 1.; +} +} + +std::string +mtx::accessors::event_id(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto e) { return e.event_id; }, event); +} +std::string +mtx::accessors::room_id(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto e) { return e.room_id; }, event); +} + +std::string +mtx::accessors::sender(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto e) { return e.sender; }, event); +} + +QDateTime +mtx::accessors::origin_server_ts(const mtx::events::collections::TimelineEvents &event) +{ + return QDateTime::fromMSecsSinceEpoch( + std::visit([](const auto e) { return e.origin_server_ts; }, event)); +} + +std::string +mtx::accessors::filename(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFilename{}, event); +} + +mtx::events::MessageType +mtx::accessors::msg_type(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMsgType{}, event); +} +std::string +mtx::accessors::room_name(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventRoomName{}, event); +} +std::string +mtx::accessors::room_topic(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventRoomTopic{}, event); +} + +std::string +mtx::accessors::body(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventBody{}, event); +} + +std::string +mtx::accessors::formatted_body(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFormattedBody{}, event); +} + +QString +mtx::accessors::formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event) +{ + auto formatted = formatted_body(event); + if (!formatted.empty()) + return QString::fromStdString(formatted); + else + return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "<br>"); +} + +std::optional<mtx::crypto::EncryptedFile> +mtx::accessors::file(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFile{}, event); +} + +std::string +mtx::accessors::url(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventUrl{}, event); +} +std::string +mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventThumbnailUrl{}, event); +} +std::string +mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMimeType{}, event); +} +std::string +mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventInReplyTo{}, event); +} + +std::string +mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventTransactionId{}, event); +} + +int64_t +mtx::accessors::filesize(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFilesize{}, event); +} + +uint64_t +mtx::accessors::media_height(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMediaHeight{}, event); +} + +uint64_t +mtx::accessors::media_width(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMediaWidth{}, event); +} diff --git a/src/EventAccessors.h b/src/EventAccessors.h new file mode 100644 index 00000000..cf79f68f --- /dev/null +++ b/src/EventAccessors.h @@ -0,0 +1,64 @@ +#pragma once + +#include <string> + +#include <QDateTime> +#include <QString> + +#include <mtx/events/collections.hpp> + +namespace mtx::accessors { +std::string +event_id(const mtx::events::collections::TimelineEvents &event); + +std::string +room_id(const mtx::events::collections::TimelineEvents &event); + +std::string +sender(const mtx::events::collections::TimelineEvents &event); + +QDateTime +origin_server_ts(const mtx::events::collections::TimelineEvents &event); + +std::string +filename(const mtx::events::collections::TimelineEvents &event); + +mtx::events::MessageType +msg_type(const mtx::events::collections::TimelineEvents &event); +std::string +room_name(const mtx::events::collections::TimelineEvents &event); +std::string +room_topic(const mtx::events::collections::TimelineEvents &event); + +std::string +body(const mtx::events::collections::TimelineEvents &event); + +std::string +formatted_body(const mtx::events::collections::TimelineEvents &event); + +QString +formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event); + +std::optional<mtx::crypto::EncryptedFile> +file(const mtx::events::collections::TimelineEvents &event); + +std::string +url(const mtx::events::collections::TimelineEvents &event); +std::string +thumbnail_url(const mtx::events::collections::TimelineEvents &event); +std::string +mimetype(const mtx::events::collections::TimelineEvents &event); +std::string +in_reply_to_event(const mtx::events::collections::TimelineEvents &event); +std::string +transaction_id(const mtx::events::collections::TimelineEvents &event); + +int64_t +filesize(const mtx::events::collections::TimelineEvents &event); + +uint64_t +media_height(const mtx::events::collections::TimelineEvents &event); + +uint64_t +media_width(const mtx::events::collections::TimelineEvents &event); +} diff --git a/src/InviteeItem.h b/src/InviteeItem.h index 85ff7a63..582904b4 100644 --- a/src/InviteeItem.h +++ b/src/InviteeItem.h @@ -3,7 +3,7 @@ #include <QLabel> #include <QWidget> -#include "mtx.hpp" +#include <mtx/identifiers.hpp> class QPushButton; diff --git a/src/Logging.cpp b/src/Logging.cpp index 686274d8..5d64a630 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -5,17 +5,56 @@ #include "spdlog/sinks/stdout_color_sinks.h" #include <iostream> +#include <QString> +#include <QtGlobal> + namespace { std::shared_ptr<spdlog::logger> db_logger = nullptr; std::shared_ptr<spdlog::logger> net_logger = nullptr; std::shared_ptr<spdlog::logger> crypto_logger = nullptr; std::shared_ptr<spdlog::logger> ui_logger = nullptr; +std::shared_ptr<spdlog::logger> qml_logger = nullptr; constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6; constexpr auto MAX_LOG_FILES = 3; + +void +qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + std::string localMsg = msg.toStdString(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + + // Surpress binding wrning for now, as we can't set restore mode to keep compat with qt 5.10 + if (msg.endsWith( + "QML Binding: Not restoring previous value because restoreMode has not been set.This " + "behavior is deprecated.In Qt < 6.0 the default is Binding.RestoreBinding.In Qt >= " + "6.0 the default is Binding.RestoreBindingOrValue.")) + return; + + switch (type) { + case QtDebugMsg: + nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtInfoMsg: + nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtWarningMsg: + nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtCriticalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtFatalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + } +} } namespace nhlog { +bool enable_debug_log_from_commandline = false; + void init(const std::string &file_path) { @@ -33,12 +72,15 @@ init(const std::string &file_path) db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks)); crypto_logger = std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks)); + qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks)); if (nheko::enable_debug_log) { db_logger->set_level(spdlog::level::trace); ui_logger->set_level(spdlog::level::trace); crypto_logger->set_level(spdlog::level::trace); } + + qInstallMessageHandler(qmlMessageHandler); } std::shared_ptr<spdlog::logger> @@ -64,4 +106,10 @@ crypto() { return crypto_logger; } + +std::shared_ptr<spdlog::logger> +qml() +{ + return qml_logger; +} } diff --git a/src/Logging.h b/src/Logging.h index 2feae60d..f572afae 100644 --- a/src/Logging.h +++ b/src/Logging.h @@ -18,4 +18,9 @@ db(); std::shared_ptr<spdlog::logger> crypto(); + +std::shared_ptr<spdlog::logger> +qml(); + +extern bool enable_debug_log_from_commandline; } diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp index f702832f..20fb3888 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp @@ -15,11 +15,14 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QPainter> #include <QStyleOption> #include <mtx/identifiers.hpp> +#include <mtx/responses/login.hpp> #include "Config.h" +#include "Logging.h" #include "LoginPage.h" #include "MatrixClient.h" #include "ui/FlatButton.h" @@ -108,7 +111,7 @@ LoginPage::LoginPage(QWidget *parent) form_layout_->addLayout(matrixidLayout_); form_layout_->addWidget(password_input_); - form_layout_->addWidget(deviceName_, Qt::AlignHCenter, 0); + form_layout_->addWidget(deviceName_, Qt::AlignHCenter, nullptr); form_layout_->addLayout(serverLayout_); button_layout_ = new QHBoxLayout(); @@ -128,6 +131,7 @@ LoginPage::LoginPage(QWidget *parent) error_label_ = new QLabel(this); error_label_->setFont(font); + error_label_->setWordWrap(true); top_layout_->addLayout(top_bar_layout_); top_layout_->addStretch(1); @@ -186,7 +190,37 @@ LoginPage::onMatrixIdEntered() serverInput_->setText(homeServer); http::client()->set_server(user.hostname()); - checkHomeserverVersion(); + http::client()->well_known([this](const mtx::responses::WellKnown &res, + mtx::http::RequestErr err) { + if (err) { + using namespace boost::beast::http; + + if (err->status_code == status::not_found) { + nhlog::net()->info("Autodiscovery: No .well-known."); + checkHomeserverVersion(); + return; + } + + if (!err->parse_error.empty()) { + emit versionErrorCb( + tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error( + "Autodiscovery failed. Received malformed response."); + return; + } + + emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known."); + return; + } + + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + + "'"); + http::client()->set_server(res.homeserver.base_url); + checkHomeserverVersion(); + }); } } @@ -272,7 +306,6 @@ LoginPage::onLoginButtonClicked() if (password_input_->text().isEmpty()) return loginError(tr("Empty password")); - http::client()->set_server(serverInput_->text().toStdString()); http::client()->login( user.localpart(), password_input_->text().toStdString(), @@ -285,6 +318,12 @@ LoginPage::onLoginButtonClicked() return; } + if (res.well_known) { + http::client()->set_server(res.well_known->homeserver.base_url); + nhlog::net()->info("Login requested to user server: " + + res.well_known->homeserver.base_url); + } + emit loginOk(res); }); diff --git a/src/LoginPage.h b/src/LoginPage.h index 99c249b1..4b84abfc 100644 --- a/src/LoginPage.h +++ b/src/LoginPage.h @@ -38,7 +38,7 @@ class LoginPage : public QWidget Q_OBJECT public: - LoginPage(QWidget *parent = 0); + LoginPage(QWidget *parent = nullptr); void reset(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 7d9a8902..fb64f0fe 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -23,6 +23,7 @@ #include <mtx/requests.hpp> +#include "Cache.h" #include "ChatPage.h" #include "Config.h" #include "Logging.h" @@ -30,6 +31,7 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "RegisterPage.h" +#include "Splitter.h" #include "TrayIcon.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -64,7 +66,7 @@ MainWindow::MainWindow(QWidget *parent) setFont(font); userSettings_ = QSharedPointer<UserSettings>(new UserSettings); - trayIcon_ = new TrayIcon(":/logos/nheko-32.png", this); + trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); welcome_page_ = new WelcomePage(this); login_page_ = new LoginPage(this); @@ -113,9 +115,6 @@ 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_, @@ -190,7 +189,7 @@ MainWindow::resizeEvent(QResizeEvent *event) void MainWindow::adjustSideBars() { - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); const uint64_t timelineWidth = chat_page_->timelineWidth(); const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups; @@ -444,7 +443,7 @@ MainWindow::openReadReceiptsDialog(const QString &event_id) const auto room_id = chat_page_->currentRoom(); try { - dialog->addUsers(cache::client()->readReceipts(event_id, room_id)); + dialog->addUsers(cache::readReceipts(event_id, room_id)); } catch (const lmdb::error &e) { nhlog::db()->warn("failed to retrieve read receipts for {} {}", event_id.toStdString(), @@ -507,4 +506,34 @@ MainWindow::loadJdenticonPlugin() nhlog::ui()->info("jdenticon plugin not found."); return false; -} \ No newline at end of file +} +void +MainWindow::showWelcomePage() +{ + removeOverlayProgressBar(); + pageStack_->addWidget(welcome_page_); + pageStack_->setCurrentWidget(welcome_page_); +} + +void +MainWindow::showLoginPage() +{ + if (modal_) + modal_->hide(); + + pageStack_->addWidget(login_page_); + pageStack_->setCurrentWidget(login_page_); +} + +void +MainWindow::showRegisterPage() +{ + pageStack_->addWidget(register_page_); + pageStack_->setCurrentWidget(register_page_); +} + +void +MainWindow::showUserSettingsPage() +{ + pageStack_->setCurrentWidget(userSettingsPage_); +} diff --git a/src/MainWindow.h b/src/MainWindow.h index 1aadbf4d..e3e04698 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -24,16 +24,17 @@ #include <QStackedWidget> #include <QSystemTrayIcon> -#include "LoginPage.h" -#include "RegisterPage.h" #include "UserSettingsPage.h" -#include "WelcomePage.h" #include "dialogs/UserProfile.h" #include "ui/OverlayModal.h" #include "jdenticoninterface.h" class ChatPage; +class RegisterPage; +class LoginPage; +class WelcomePage; + class LoadingIndicator; class OverlayModal; class SnackBar; @@ -62,7 +63,7 @@ class MainWindow : public QMainWindow Q_OBJECT public: - explicit MainWindow(QWidget *parent = 0); + explicit MainWindow(QWidget *parent = nullptr); static MainWindow *instance() { return instance_; }; void saveCurrentWindowSize(); @@ -97,32 +98,16 @@ private slots: void iconActivated(QSystemTrayIcon::ActivationReason reason); //! Show the welcome page in the main window. - void showWelcomePage() - { - removeOverlayProgressBar(); - pageStack_->addWidget(welcome_page_); - pageStack_->setCurrentWidget(welcome_page_); - } + void showWelcomePage(); //! Show the login page in the main window. - void showLoginPage() - { - if (modal_) - modal_->hide(); - - pageStack_->addWidget(login_page_); - pageStack_->setCurrentWidget(login_page_); - } + void showLoginPage(); //! Show the register page in the main window. - void showRegisterPage() - { - pageStack_->addWidget(register_page_); - pageStack_->setCurrentWidget(register_page_); - } + void showRegisterPage(); //! Show user settings page. - void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); } + void showUserSettingsPage(); //! Show the chat page and start communicating with the given access token. void showChatPage(); diff --git a/src/MatrixClient.cpp b/src/MatrixClient.cpp index 12d7ac91..b69ba480 100644 --- a/src/MatrixClient.cpp +++ b/src/MatrixClient.cpp @@ -2,6 +2,26 @@ #include <memory> +#include <QMetaType> +#include <QObject> +#include <QString> + +#include "nlohmann/json.hpp" +#include <mtx/responses.hpp> + +Q_DECLARE_METATYPE(mtx::responses::Login) +Q_DECLARE_METATYPE(mtx::responses::Messages) +Q_DECLARE_METATYPE(mtx::responses::Notifications) +Q_DECLARE_METATYPE(mtx::responses::Rooms) +Q_DECLARE_METATYPE(mtx::responses::Sync) +Q_DECLARE_METATYPE(mtx::responses::JoinedGroups) +Q_DECLARE_METATYPE(mtx::responses::GroupProfile) + +Q_DECLARE_METATYPE(nlohmann::json) +Q_DECLARE_METATYPE(std::string) +Q_DECLARE_METATYPE(std::vector<std::string>) +Q_DECLARE_METATYPE(std::vector<QString>) + namespace { auto client_ = std::make_shared<mtx::http::Client>(); } diff --git a/src/MatrixClient.h b/src/MatrixClient.h index 2af57267..4db51095 100644 --- a/src/MatrixClient.h +++ b/src/MatrixClient.h @@ -1,35 +1,7 @@ #pragma once -#include <QMetaType> -#include <QObject> -#include <QString> - -#include "nlohmann/json.hpp" -#include <mtx/responses.hpp> #include <mtxclient/http/client.hpp> -Q_DECLARE_METATYPE(mtx::responses::Login) -Q_DECLARE_METATYPE(mtx::responses::Messages) -Q_DECLARE_METATYPE(mtx::responses::Notifications) -Q_DECLARE_METATYPE(mtx::responses::Rooms) -Q_DECLARE_METATYPE(mtx::responses::Sync) -Q_DECLARE_METATYPE(mtx::responses::JoinedGroups) -Q_DECLARE_METATYPE(mtx::responses::GroupProfile) -Q_DECLARE_METATYPE(std::string) -Q_DECLARE_METATYPE(nlohmann::json) -Q_DECLARE_METATYPE(std::vector<std::string>) -Q_DECLARE_METATYPE(std::vector<QString>) - -class MediaProxy : public QObject -{ - Q_OBJECT - -signals: - void imageDownloaded(const QPixmap &); - void imageSaved(const QString &, const QByteArray &); - void fileDownloaded(const QByteArray &); -}; - namespace http { mtx::http::Client * client(); diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp new file mode 100644 index 00000000..d04eab24 --- /dev/null +++ b/src/MxcImageProvider.cpp @@ -0,0 +1,85 @@ +#include "MxcImageProvider.h" + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +void +MxcImageResponse::run() +{ + if (m_requestedSize.isValid() && !m_encryptionInfo) { + QString fileName = QString("%1_%2x%3_crop") + .arg(m_id) + .arg(m_requestedSize.width()) + .arg(m_requestedSize.height()); + + auto data = cache::image(fileName); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + mtx::http::ThumbOpts opts; + opts.mxc_url = "mxc://" + m_id.toStdString(); + opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; + opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; + opts.method = "crop"; + http::client()->get_thumbnail( + opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto data = QByteArray(res.data(), res.size()); + cache::saveImage(fileName, data); + m_image.loadFromData(data); + m_image.setText("mxc url", "mxc://" + m_id); + + emit finished(); + }); + } else { + auto data = cache::image(m_id); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + http::client()->download( + "mxc://" + m_id.toStdString(), + [this](const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto temp = res; + if (m_encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); + + auto data = QByteArray(temp.data(), temp.size()); + m_image.loadFromData(data); + m_image.setText("original filename", + QString::fromStdString(originalFilename)); + m_image.setText("mxc url", "mxc://" + m_id); + cache::saveImage(m_id, data); + + emit finished(); + }); + } +} diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h new file mode 100644 index 00000000..2c197a13 --- /dev/null +++ b/src/MxcImageProvider.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QQuickAsyncImageProvider> +#include <QQuickImageResponse> + +#include <QImage> +#include <QThreadPool> + +#include <mtx/common.hpp> + +#include <boost/optional.hpp> + +class MxcImageResponse + : public QQuickImageResponse + , public QRunnable +{ +public: + MxcImageResponse(const QString &id, + const QSize &requestedSize, + boost::optional<mtx::crypto::EncryptedFile> encryptionInfo) + : m_id(id) + , m_requestedSize(requestedSize) + , m_encryptionInfo(encryptionInfo) + { + setAutoDelete(false); + } + + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + QString errorString() const override { return m_error; } + + void run() override; + + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; + boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo; +}; + +class MxcImageProvider + : public QObject + , public QQuickAsyncImageProvider +{ + Q_OBJECT +public slots: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + boost::optional<mtx::crypto::EncryptedFile> info; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + info = *temp; + + MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); + pool.start(response); + return response; + } + + void addEncryptionInfo(mtx::crypto::EncryptedFile info) + { + infos.insert(QString::fromStdString(info.url), info); + } + +private: + QThreadPool pool; + QHash<QString, mtx::crypto::EncryptedFile> infos; +}; diff --git a/src/Olm.cpp b/src/Olm.cpp index c1598570..78b16be7 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -1,4 +1,4 @@ -#include <boost/variant.hpp> +#include <variant> #include "Olm.h" @@ -121,7 +121,7 @@ handle_pre_key_olm_message(const std::string &sender, // We also remove the one time key used to establish that // session so we'll have to update our copy of the account object. - cache::client()->saveOlmAccount(olm::client()->save("secret")); + cache::saveOlmAccount(olm::client()->save("secret")); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to create inbound session with {}: {}", sender, e.what()); @@ -149,7 +149,7 @@ handle_pre_key_olm_message(const std::string &sender, nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2)); try { - cache::client()->saveOlmSession(sender_key, std::move(inbound_session)); + cache::saveOlmSession(sender_key, std::move(inbound_session)); } catch (const lmdb::error &e) { nhlog::db()->warn( "failed to save inbound olm session from {}: {}", sender, e.what()); @@ -159,15 +159,20 @@ handle_pre_key_olm_message(const std::string &sender, } mtx::events::msg::Encrypted -encrypt_group_message(const std::string &room_id, - const std::string &device_id, - const std::string &body) +encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body) { using namespace mtx::events; - // Always chech before for existence. - auto res = cache::client()->getOutboundMegolmSession(room_id); - auto payload = olm::client()->encrypt_group_message(res.session, body); + // relations shouldn't be encrypted... + mtx::common::RelatesTo relation; + if (body["content"].count("m.relates_to") != 0) { + relation = body["content"]["m.relates_to"]; + body["content"].erase("m.relates_to"); + } + + // Always check before for existence. + auto res = cache::getOutboundMegolmSession(room_id); + auto payload = olm::client()->encrypt_group_message(res.session, body.dump()); // Prepare the m.room.encrypted event. msg::Encrypted data; @@ -176,12 +181,13 @@ encrypt_group_message(const std::string &room_id, data.session_id = res.data.session_id; data.device_id = device_id; data.algorithm = MEGOLM_ALGO; + data.relates_to = relation; auto message_index = olm_outbound_group_session_message_index(res.session); nhlog::crypto()->info("next message_index {}", message_index); // We need to re-pickle the session after we send a message to save the new message_index. - cache::client()->updateOutboundMegolmSession(room_id, message_index); + cache::updateOutboundMegolmSession(room_id, message_index); return data; } @@ -189,13 +195,13 @@ encrypt_group_message(const std::string &room_id, nlohmann::json try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &msg) { - auto session_ids = cache::client()->getOlmSessions(sender_key); + auto session_ids = cache::getOlmSessions(sender_key); nhlog::crypto()->info("attempt to decrypt message with {} known session_ids", session_ids.size()); for (const auto &id : session_ids) { - auto session = cache::client()->getOlmSession(sender_key, id); + auto session = cache::getOlmSession(sender_key, id); if (!session) continue; @@ -204,7 +210,7 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip try { text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); - cache::client()->saveOlmSession(id, std::move(session.value())); + cache::saveOlmSession(id, std::move(session.value())); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}", msg.type, @@ -252,7 +258,7 @@ create_inbound_megolm_session(const std::string &sender, try { auto megolm_session = olm::client()->init_inbound_group_session(session_key); - cache::client()->saveInboundMegolmSession(index, std::move(megolm_session)); + cache::saveInboundMegolmSession(index, std::move(megolm_session)); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; @@ -268,7 +274,7 @@ void mark_keys_as_published() { olm::client()->mark_keys_as_published(); - cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); } void @@ -289,14 +295,13 @@ request_keys(const std::string &room_id, const std::string &event_id) return; } - if (boost::get<EncryptedEvent<msg::Encrypted>>(&res) == nullptr) { + if (!std::holds_alternative<EncryptedEvent<msg::Encrypted>>(res)) { nhlog::net()->info( "retrieved event is not encrypted: {} from {}", event_id, room_id); return; } - olm::send_key_request_for(room_id, - boost::get<EncryptedEvent<msg::Encrypted>>(res)); + olm::send_key_request_for(room_id, std::get<EncryptedEvent<msg::Encrypted>>(res)); }); } @@ -356,13 +361,13 @@ handle_key_request_message(const mtx::events::msg::KeyRequest &req) } // Check if we have the keys for the requested session. - if (!cache::client()->outboundMegolmSessionExists(req.room_id)) { + if (!cache::outboundMegolmSessionExists(req.room_id)) { nhlog::crypto()->warn("requested session not found in room: {}", req.room_id); return; } // Check that the requested session_id and the one we have saved match. - const auto session = cache::client()->getOutboundMegolmSession(req.room_id); + const auto session = cache::getOutboundMegolmSession(req.room_id); if (req.session_id != session.data.session_id) { nhlog::crypto()->warn("session id of retrieved session doesn't match the request: " "requested({}), ours({})", @@ -371,7 +376,7 @@ handle_key_request_message(const mtx::events::msg::KeyRequest &req) return; } - if (!cache::client()->isRoomMember(req.sender, req.room_id)) { + if (!cache::isRoomMember(req.sender, req.room_id)) { nhlog::crypto()->warn( "user {} that requested the session key is not member of the room {}", req.sender, @@ -510,8 +515,7 @@ send_megolm_key_to_device(const std::string &user_id, device_msg = olm::client()->create_olm_encrypted_content( olm_session.get(), room_key, pks.curve25519); - cache::client()->saveOlmSession(pks.curve25519, - std::move(olm_session)); + cache::saveOlmSession(pks.curve25519, std::move(olm_session)); } catch (const json::exception &e) { nhlog::crypto()->warn("creating outbound session: {}", e.what()); diff --git a/src/Olm.h b/src/Olm.h index ae4e0659..28521413 100644 --- a/src/Olm.h +++ b/src/Olm.h @@ -3,7 +3,8 @@ #include <boost/optional.hpp> #include <memory> -#include <mtx.hpp> +#include <mtx/events.hpp> +#include <mtx/events/encrypted.hpp> #include <mtxclient/crypto/client.hpp> constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; @@ -62,7 +63,7 @@ handle_pre_key_olm_message(const std::string &sender, mtx::events::msg::Encrypted encrypt_group_message(const std::string &room_id, const std::string &device_id, - const std::string &body); + nlohmann::json body); void mark_keys_as_published(); diff --git a/src/QuickSwitcher.cpp b/src/QuickSwitcher.cpp index eb79a427..05a9f431 100644 --- a/src/QuickSwitcher.cpp +++ b/src/QuickSwitcher.cpp @@ -22,8 +22,11 @@ #include <QTimer> #include <QtConcurrent> +#include "Cache.h" #include "QuickSwitcher.h" -#include "SuggestionsPopup.h" +#include "popups/SuggestionsPopup.h" + +Q_DECLARE_METATYPE(std::vector<RoomSearchResult>) RoomSearchInput::RoomSearchInput(QWidget *parent) : TextField(parent) @@ -93,8 +96,7 @@ QuickSwitcher::QuickSwitcher(QWidget *parent) QtConcurrent::run([this, query = query.toLower()]() { try { - emit queryResults( - cache::client()->searchRooms(query.toStdString())); + emit queryResults(cache::searchRooms(query.toStdString())); } catch (const lmdb::error &e) { qWarning() << "room search failed:" << e.what(); } diff --git a/src/QuickSwitcher.h b/src/QuickSwitcher.h index 24b9adfa..5bc31650 100644 --- a/src/QuickSwitcher.h +++ b/src/QuickSwitcher.h @@ -22,11 +22,9 @@ #include <QVBoxLayout> #include <QWidget> -#include "SuggestionsPopup.h" +#include "popups/SuggestionsPopup.h" #include "ui/TextField.h" -Q_DECLARE_METATYPE(std::vector<RoomSearchResult>) - class RoomSearchInput : public TextField { Q_OBJECT diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp index fdb0f43a..39a69a34 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp @@ -15,9 +15,13 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QMetaType> +#include <QPainter> #include <QStyleOption> #include <QTimer> +#include <mtx/responses/register.hpp> + #include "Config.h" #include "Logging.h" #include "MainWindow.h" @@ -27,11 +31,17 @@ #include "ui/RaisedButton.h" #include "ui/TextField.h" +#include "dialogs/FallbackAuth.h" #include "dialogs/ReCaptcha.h" +Q_DECLARE_METATYPE(mtx::user_interactive::Unauthorized) +Q_DECLARE_METATYPE(mtx::user_interactive::Auth) + RegisterPage::RegisterPage(QWidget *parent) : QWidget(parent) { + qRegisterMetaType<mtx::user_interactive::Unauthorized>(); + qRegisterMetaType<mtx::user_interactive::Auth>(); top_layout_ = new QVBoxLayout(); back_layout_ = new QHBoxLayout(); @@ -87,10 +97,10 @@ RegisterPage::RegisterPage(QWidget *parent) server_input_ = new TextField(); server_input_->setLabel(tr("Home Server")); - form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0); - form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); - form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, 0); - form_layout_->addWidget(server_input_, Qt::AlignHCenter, 0); + form_layout_->addWidget(username_input_, Qt::AlignHCenter, nullptr); + form_layout_->addWidget(password_input_, Qt::AlignHCenter, nullptr); + form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, nullptr); + form_layout_->addWidget(server_input_, Qt::AlignHCenter, nullptr); button_layout_ = new QHBoxLayout(); button_layout_->setSpacing(0); @@ -130,46 +140,139 @@ RegisterPage::RegisterPage(QWidget *parent) this, &RegisterPage::registrationFlow, this, - [this](const std::string &user, const std::string &pass, const std::string &session) { - emit errorOccurred(); - - auto captchaDialog = - new dialogs::ReCaptcha(QString::fromStdString(session), this); - - connect(captchaDialog, - &dialogs::ReCaptcha::confirmation, - this, - [this, user, pass, session, captchaDialog]() { - captchaDialog->close(); - captchaDialog->deleteLater(); - - emit registering(); - - http::client()->flow_response( - user, - pass, - session, - "m.login.recaptcha", - [this](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve registration flows: {}", - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb(QString::fromStdString( - err->matrix_error.error)); - return; - } - - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - - emit registerOk(); - }); - }); - - QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); }); + [this](const std::string &user, + const std::string &pass, + const mtx::user_interactive::Unauthorized &unauthorized) { + auto completed_stages = unauthorized.completed; + auto flows = unauthorized.flows; + auto session = unauthorized.session; + + nhlog::ui()->info("Completed stages: {}", completed_stages.size()); + + if (!completed_stages.empty()) + flows.erase(std::remove_if( + flows.begin(), + flows.end(), + [completed_stages](auto flow) { + if (completed_stages.size() > flow.stages.size()) + return true; + for (size_t f = 0; f < completed_stages.size(); f++) + if (completed_stages[f] != flow.stages[f]) + return true; + return false; + }), + flows.end()); + + if (flows.empty()) { + nhlog::net()->error("No available registration flows!"); + emit registerErrorCb(tr("No supported registration flows!")); + return; + } + + auto current_stage = flows.front().stages.at(completed_stages.size()); + + if (current_stage == mtx::user_interactive::auth_types::recaptcha) { + auto captchaDialog = + new dialogs::ReCaptcha(QString::fromStdString(session), this); + + connect(captchaDialog, + &dialogs::ReCaptcha::confirmation, + this, + [this, user, pass, session, captchaDialog]() { + captchaDialog->close(); + captchaDialog->deleteLater(); + + emit registerAuth( + user, + pass, + mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + connect(captchaDialog, + &dialogs::ReCaptcha::cancel, + this, + &RegisterPage::errorOccurred); + + QTimer::singleShot( + 1000, this, [captchaDialog]() { captchaDialog->show(); }); + } else if (current_stage == mtx::user_interactive::auth_types::dummy) { + emit registerAuth(user, + pass, + mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Dummy{}}); + } else { + // use fallback + auto dialog = + new dialogs::FallbackAuth(QString::fromStdString(current_stage), + QString::fromStdString(session), + this); + + connect(dialog, + &dialogs::FallbackAuth::confirmation, + this, + [this, user, pass, session, dialog]() { + dialog->close(); + dialog->deleteLater(); + + emit registerAuth( + user, + pass, + mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + connect(dialog, + &dialogs::FallbackAuth::cancel, + this, + &RegisterPage::errorOccurred); + + dialog->show(); + } + }); + + connect( + this, + &RegisterPage::registerAuth, + this, + [this](const std::string &user, + const std::string &pass, + const mtx::user_interactive::Auth &auth) { + http::client()->registration( + user, + pass, + auth, + [this, user, pass](const mtx::responses::Register &res, + mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + + emit registerOk(); + return; + } + + // The server requires registration flows. + if (err->status_code == boost::beast::http::status::unauthorized) { + if (err->matrix_error.unauthorized.session.empty()) { + nhlog::net()->warn( + "failed to retrieve registration flows: ({}) " + "{}", + static_cast<int>(err->status_code), + err->matrix_error.error); + emit registerErrorCb( + QString::fromStdString(err->matrix_error.error)); + return; + } + + emit registrationFlow( + user, pass, err->matrix_error.unauthorized); + return; + } + + nhlog::net()->warn("failed to register: status_code ({})", + static_cast<int>(err->status_code)); + + emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); + }); }); setLayout(top_layout_); @@ -222,31 +325,27 @@ RegisterPage::onRegisterButtonClicked() // The server requires registration flows. if (err->status_code == boost::beast::http::status::unauthorized) { - http::client()->flow_register( - username, - password, - [this, username, password]( - const mtx::responses::RegistrationFlows &res, - mtx::http::RequestErr err) { - if (res.session.empty() && err) { - nhlog::net()->warn( - "failed to retrieve registration flows: ({}) " - "{}", - static_cast<int>(err->status_code), - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb(QString::fromStdString( - err->matrix_error.error)); - return; - } - - emit registrationFlow(username, password, res.session); - }); + if (err->matrix_error.unauthorized.session.empty()) { + nhlog::net()->warn( + "failed to retrieve registration flows: ({}) " + "{}", + static_cast<int>(err->status_code), + err->matrix_error.error); + emit errorOccurred(); + emit registerErrorCb( + QString::fromStdString(err->matrix_error.error)); + return; + } + + emit registrationFlow( + username, password, err->matrix_error.unauthorized); return; } - nhlog::net()->warn("failed to register: status_code ({})", - static_cast<int>(err->status_code)); + nhlog::net()->warn( + "failed to register: status_code ({}), matrix_error({})", + static_cast<int>(err->status_code), + err->matrix_error.error); emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); emit errorOccurred(); diff --git a/src/RegisterPage.h b/src/RegisterPage.h index b05cf150..ebc24bb1 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h @@ -21,6 +21,8 @@ #include <QLayout> #include <memory> +#include <mtx/user_interactive.hpp> + class FlatButton; class RaisedButton; class TextField; @@ -30,7 +32,7 @@ class RegisterPage : public QWidget Q_OBJECT public: - RegisterPage(QWidget *parent = 0); + RegisterPage(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; @@ -43,7 +45,10 @@ signals: void registerErrorCb(const QString &msg); void registrationFlow(const std::string &user, const std::string &pass, - const std::string &session); + const mtx::user_interactive::Unauthorized &unauthorized); + void registerAuth(const std::string &user, + const std::string &pass, + const mtx::user_interactive::Auth &auth); private slots: void onBackButtonClicked(); diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index f17b383c..4c8535bf 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -16,13 +16,16 @@ */ #include <QDateTime> -#include <QDebug> #include <QMouseEvent> #include <QPainter> +#include <QSettings> +#include <QtGlobal> +#include "AvatarProvider.h" #include "Cache.h" #include "Config.h" #include "RoomInfoListItem.h" +#include "Splitter.h" #include "Utils.h" #include "ui/Menu.h" #include "ui/Ripple.h" @@ -30,9 +33,6 @@ constexpr int MaxUnreadCountDisplayed = 99; -constexpr int IconSize = 44; -// constexpr int MaxHeight = IconSize + 2 * Padding; - struct WidgetMetrics { int maxHeight; @@ -62,7 +62,7 @@ getMetrics(const QFont &font) m.unreadLineOffset = m.padding - m.padding / 4; m.inviteBtnX = m.iconSize + 2 * m.padding; - m.inviteBtnX = m.iconSize / 2.0 + m.padding + m.padding / 3.0; + m.inviteBtnY = m.iconSize / 2.0 + m.padding + m.padding / 3.0; return m; } @@ -74,7 +74,8 @@ RoomInfoListItem::init(QWidget *parent) setMouseTracking(true); setAttribute(Qt::WA_Hover); - setFixedHeight(getMetrics(QFont{}).maxHeight); + auto wm = getMetrics(QFont{}); + setFixedHeight(wm.maxHeight); QPainterPath path; path.addRect(0, 0, parent->width(), height()); @@ -83,6 +84,10 @@ RoomInfoListItem::init(QWidget *parent) ripple_overlay_->setClipPath(path); ripple_overlay_->setClipping(true); + avatar_ = new Avatar(this, wm.iconSize); + avatar_->setLetter(utils::firstChar(roomName_)); + avatar_->move(wm.padding, wm.padding); + unreadCountFont_.setPointSizeF(unreadCountFont_.pointSizeF() * 0.8); unreadCountFont_.setBold(true); @@ -94,7 +99,7 @@ RoomInfoListItem::init(QWidget *parent) menu_->addAction(leaveRoom_); } -RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent) +RoomInfoListItem::RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent) : QWidget(parent) , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined} , roomId_(std::move(room_id)) @@ -104,18 +109,6 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare , unreadHighlightedMsgCount_(0) { init(parent); - - QString emptyEventId; - - // HACK - // We use fake message info with an old date to pin - // the invite events to the top. - // - // State events in invited rooms don't contain timestamp info, - // so we can't use them for sorting. - if (roomType_ == RoomType::Invited) - lastMsgInfo_ = { - emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; } void @@ -125,7 +118,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *) QPainterPath path; path.addRect(0, 0, width(), height()); - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() > sidebarSizes.small) setToolTip(""); @@ -167,12 +160,10 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) subtitlePen.setColor(subtitleColor_); } - QRect avatarRegion(wm.padding, wm.padding, wm.iconSize, wm.iconSize); - // Description line with the default font. int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2; - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() > sidebarSizes.small) { QFont headingFont; @@ -182,8 +173,12 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) QFont tsFont; tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9); +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) const int msgStampWidth = QFontMetrics(tsFont).width(lastMsgInfo_.timestamp) + 4; - +#else + const int msgStampWidth = + QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.timestamp) + 4; +#endif // We use the full width of the widget if there is no unread msg bubble. const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; @@ -201,30 +196,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) p.setFont(QFont{}); p.setPen(subtitlePen); - // The limit is the space between the end of the avatar and the start of the - // timestamp. - int usernameLimit = - std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20); - auto userName = - metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit); - - p.setFont(QFont{}); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName); - - int nameWidth = QFontMetrics(QFont{}).width(userName); - - p.setFont(QFont{}); - - // The limit is the space between the end of the username and the start of - // the timestamp. - int descriptionLimit = - std::max(0, - width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize - - nameWidth - 5); + int descriptionLimit = std::max( + 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize); auto description = metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); - p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y), - description); + p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description); // We show the last message timestamp. p.save(); @@ -263,42 +239,17 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) p.setPen(QPen(btnTextColor_)); p.setFont(QFont{}); - p.drawText(acceptBtnRegion_, Qt::AlignCenter, tr("Accept")); - p.drawText(declineBtnRegion_, Qt::AlignCenter, tr("Decline")); + p.drawText(acceptBtnRegion_, + Qt::AlignCenter, + metrics.elidedText(tr("Accept"), Qt::ElideRight, btnWidth)); + p.drawText(declineBtnRegion_, + Qt::AlignCenter, + metrics.elidedText(tr("Decline"), Qt::ElideRight, btnWidth)); } } p.setPen(Qt::NoPen); - // We using the first letter of room's name. - if (roomAvatar_.isNull()) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(avatarBgColor()); - - p.setPen(Qt::NoPen); - p.setBrush(brush); - - p.drawEllipse(avatarRegion.center(), wm.iconSize / 2, wm.iconSize / 2); - - QFont bubbleFont; - bubbleFont.setPointSizeF(bubbleFont.pointSizeF() * 1.4); - p.setFont(bubbleFont); - p.setPen(avatarFgColor()); - p.setBrush(Qt::NoBrush); - p.drawText( - avatarRegion.translated(0, -1), Qt::AlignCenter, utils::firstChar(roomName())); - } else { - p.save(); - - QPainterPath path; - path.addEllipse(wm.padding, wm.padding, wm.iconSize, wm.iconSize); - p.setClipPath(path); - - p.drawPixmap(avatarRegion, roomAvatar_); - p.restore(); - } - if (unreadMsgCount_ > 0) { QBrush brush; brush.setStyle(Qt::SolidPattern); @@ -426,10 +377,9 @@ RoomInfoListItem::mousePressEvent(QMouseEvent *event) } void -RoomInfoListItem::setAvatar(const QImage &img) +RoomInfoListItem::setAvatar(const QString &avatar_url) { - roomAvatar_ = utils::scaleImageToPixmap(img, IconSize); - update(); + avatar_->setImage(avatar_url); } void diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h index 40c938c1..c1ee533d 100644 --- a/src/RoomInfoListItem.h +++ b/src/RoomInfoListItem.h @@ -22,9 +22,11 @@ #include <QSharedPointer> #include <QWidget> -#include "Cache.h" #include <mtx/responses.hpp> +#include "CacheStructs.h" +#include "ui/Avatar.h" + class Menu; class RippleOverlay; @@ -37,9 +39,6 @@ class RoomInfoListItem : public QWidget QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - Q_PROPERTY(QColor avatarBgColor READ avatarBgColor WRITE setAvatarBgColor) - Q_PROPERTY(QColor avatarFgColor READ avatarFgColor WRITE setAvatarFgColor) - Q_PROPERTY(QColor bubbleBgColor READ bubbleBgColor WRITE setBubbleBgColor) Q_PROPERTY(QColor bubbleFgColor READ bubbleFgColor WRITE setBubbleFgColor) @@ -64,7 +63,7 @@ class RoomInfoListItem : public QWidget Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) public: - RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent = 0); + RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr); void updateUnreadMessageCount(int count, int highlightedCount); void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); }; @@ -73,7 +72,7 @@ public: bool isPressed() const { return isPressed_; } int unreadMessageCount() const { return unreadMsgCount_; } - void setAvatar(const QImage &avatar_image); + void setAvatar(const QString &avatar_url); void setDescriptionMessage(const DescInfo &info); DescInfo lastMessageInfo() const { return lastMsgInfo_; } @@ -83,8 +82,6 @@ public: QColor hoverSubtitleColor() const { return hoverSubtitleColor_; } QColor hoverTimestampColor() const { return hoverTimestampColor_; } QColor backgroundColor() const { return backgroundColor_; } - QColor avatarBgColor() const { return avatarBgColor_; } - QColor avatarFgColor() const { return avatarFgColor_; } QColor highlightedTitleColor() const { return highlightedTitleColor_; } QColor highlightedSubtitleColor() const { return highlightedSubtitleColor_; } @@ -107,8 +104,6 @@ public: void setHoverTimestampColor(QColor &color) { hoverTimestampColor_ = color; } void setBackgroundColor(QColor &color) { backgroundColor_ = color; } void setTimestampColor(QColor &color) { timestampColor_ = color; } - void setAvatarFgColor(QColor &color) { avatarFgColor_ = color; } - void setAvatarBgColor(QColor &color) { avatarBgColor_ = color; } void setHighlightedTitleColor(QColor &color) { highlightedTitleColor_ = color; } void setHighlightedSubtitleColor(QColor &color) { highlightedSubtitleColor_ = color; } @@ -162,6 +157,7 @@ private: QString roomName() { return roomName_; } RippleOverlay *ripple_overlay_; + Avatar *avatar_; enum class RoomType { @@ -179,8 +175,6 @@ private: DescInfo lastMsgInfo_; - QPixmap roomAvatar_; - Menu *menu_; QAction *leaveRoom_; @@ -218,9 +212,6 @@ private: QColor highlightedTimestampColor_; QColor hoverTimestampColor_; - QColor avatarBgColor_; - QColor avatarFgColor_; - QColor bubbleBgColor_; QColor bubbleFgColor_; }; diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 1abf3533..6feb4f76 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -15,18 +15,17 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QApplication> -#include <QBuffer> +#include <limits> + #include <QObject> +#include <QPainter> +#include <QScroller> #include <QTimer> -#include "Cache.h" #include "Logging.h" #include "MainWindow.h" -#include "MatrixClient.h" #include "RoomInfoListItem.h" #include "RoomList.h" -#include "UserSettingsPage.h" #include "Utils.h" #include "ui/OverlayModal.h" @@ -43,6 +42,8 @@ RoomList::RoomList(QWidget *parent) scrollArea_->setWidgetResizable(true); scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); + QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); + // The scrollbar on macOS will hide itself when not active so it won't interfere // with the content. #if not defined(Q_OS_MAC) @@ -89,40 +90,7 @@ RoomList::updateAvatar(const QString &room_id, const QString &url) if (url.isEmpty()) return; - QByteArray savedImgData; - - if (cache::client()) - savedImgData = cache::client()->image(url); - - if (savedImgData.isEmpty()) { - mtx::http::ThumbOpts opts; - opts.mxc_url = url.toStdString(); - http::client()->get_thumbnail( - opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download room avatar: {} {} {}", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), res.size()); - QPixmap pixmap; - pixmap.loadFromData(data); - - emit updateRoomAvatarCb(room_id, pixmap); - }); - } else { - QPixmap img; - img.loadFromData(savedImgData); - - updateRoomAvatar(room_id, img); - } + emit updateRoomAvatarCb(room_id, url); } void @@ -193,6 +161,8 @@ RoomList::initialize(const QMap<QString, RoomInfo> &info) if (rooms_.empty()) return; + sortRoomsByLastMessage(); + auto room = firstRoom(); if (room.second.isNull()) return; @@ -224,6 +194,9 @@ RoomList::sync(const std::map<QString, RoomInfo> &info) { for (const auto &room : info) updateRoom(room.first, room.second); + + if (!info.empty()) + sortRoomsByLastMessage(); } void @@ -252,7 +225,73 @@ RoomList::highlightSelectedRoom(const QString &room_id) } void -RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) +RoomList::nextRoom() +{ + for (int ii = 0; ii < contentsLayout_->count() - 1; ++ii) { + auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); + + if (!room) + continue; + + if (room->roomId() == selectedRoom_) { + auto nextRoom = qobject_cast<RoomInfoListItem *>( + contentsLayout_->itemAt(ii + 1)->widget()); + + // Not a room message. + if (!nextRoom || nextRoom->isInvite()) + return; + + emit roomChanged(nextRoom->roomId()); + if (!roomExists(nextRoom->roomId())) { + nhlog::ui()->warn("roomlist: clicked unknown room_id"); + return; + } + + room->setPressedState(false); + nextRoom->setPressedState(true); + + scrollArea_->ensureWidgetVisible(nextRoom); + selectedRoom_ = nextRoom->roomId(); + return; + } + } +} + +void +RoomList::previousRoom() +{ + for (int ii = 1; ii < contentsLayout_->count(); ++ii) { + auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); + + if (!room) + continue; + + if (room->roomId() == selectedRoom_) { + auto nextRoom = qobject_cast<RoomInfoListItem *>( + contentsLayout_->itemAt(ii - 1)->widget()); + + // Not a room message. + if (!nextRoom || nextRoom->isInvite()) + return; + + emit roomChanged(nextRoom->roomId()); + if (!roomExists(nextRoom->roomId())) { + nhlog::ui()->warn("roomlist: clicked unknown room_id"); + return; + } + + room->setPressedState(false); + nextRoom->setPressedState(true); + + scrollArea_->ensureWidgetVisible(nextRoom); + selectedRoom_ = nextRoom->roomId(); + return; + } + } +} + +void +RoomList::updateRoomAvatar(const QString &roomid, const QString &img) { if (!roomExists(roomid)) { nhlog::ui()->warn("avatar update on non-existent room_id: {}", @@ -260,7 +299,7 @@ RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) return; } - rooms_[roomid]->setAvatar(img.toImage()); + rooms_[roomid]->setAvatar(img); // Used to inform other widgets for the new image data. emit roomAvatarChanged(roomid, img); @@ -303,7 +342,9 @@ RoomList::sortRoomsByLastMessage() continue; // Not a room message. - if (room->lastMessageInfo().userid.isEmpty()) + if (room->isInvite()) + times.emplace(std::numeric_limits<uint64_t>::max(), room); + else if (room->lastMessageInfo().userid.isEmpty()) times.emplace(0, room); else times.emplace(room->lastMessageInfo().datetime.toMSecsSinceEpoch(), room); @@ -443,13 +484,16 @@ RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info) std::pair<QString, QSharedPointer<RoomInfoListItem>> RoomList::firstRoom() const { - auto firstRoom = rooms_.begin(); + for (int i = 0; i < contentsLayout_->count(); i++) { + auto item = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - while (firstRoom->second.isNull() && firstRoom != rooms_.end()) - firstRoom++; + if (item) { + return std::pair<QString, QSharedPointer<RoomInfoListItem>>( + item->roomId(), rooms_.at(item->roomId())); + } + } - return std::pair<QString, QSharedPointer<RoomInfoListItem>>(firstRoom->first, - firstRoom->second); + return {}; } void diff --git a/src/RoomList.h b/src/RoomList.h index 155a969c..fef552c6 100644 --- a/src/RoomList.h +++ b/src/RoomList.h @@ -17,15 +17,12 @@ #pragma once -#include <QMetaType> #include <QPushButton> #include <QScrollArea> #include <QSharedPointer> #include <QVBoxLayout> #include <QWidget> -#include <mtx.hpp> - class LeaveRoomDialog; class OverlayModal; class RoomInfoListItem; @@ -38,7 +35,7 @@ class RoomList : public QWidget Q_OBJECT public: - explicit RoomList(QWidget *parent = 0); + explicit RoomList(QWidget *parent = nullptr); void initialize(const QMap<QString, RoomInfo> &info); void sync(const std::map<QString, RoomInfo> &info); @@ -61,17 +58,19 @@ signals: void totalUnreadMessageCountUpdated(int count); void acceptInvite(const QString &room_id); void declineInvite(const QString &room_id); - void roomAvatarChanged(const QString &room_id, const QPixmap &img); + void roomAvatarChanged(const QString &room_id, const QString &img); void joinRoom(const QString &room_id); - void updateRoomAvatarCb(const QString &room_id, const QPixmap &img); + void updateRoomAvatarCb(const QString &room_id, const QString &img); public slots: - void updateRoomAvatar(const QString &roomid, const QPixmap &img); + void updateRoomAvatar(const QString &roomid, const QString &img); void highlightSelectedRoom(const QString &room_id); 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); + void nextRoom(); + void previousRoom(); protected: void paintEvent(QPaintEvent *event) override; diff --git a/src/RunGuard.cpp b/src/RunGuard.cpp deleted file mode 100644 index 75833eb7..00000000 --- a/src/RunGuard.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "RunGuard.h" - -#include <QCryptographicHash> - -namespace { - -QString -generateKeyHash(const QString &key, const QString &salt) -{ - QByteArray data; - - data.append(key.toUtf8()); - data.append(salt.toUtf8()); - data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); - - return data; -} -} - -RunGuard::RunGuard(const QString &key) - : key(key) - , memLockKey(generateKeyHash(key, "_memLockKey")) - , sharedmemKey(generateKeyHash(key, "_sharedmemKey")) - , sharedMem(sharedmemKey) - , memLock(memLockKey, 1) -{ - memLock.acquire(); - { - // Fix for *nix: http://habrahabr.ru/post/173281/ - QSharedMemory fix(sharedmemKey); - fix.attach(); - } - - memLock.release(); -} - -RunGuard::~RunGuard() { release(); } - -bool -RunGuard::isAnotherRunning() -{ - if (sharedMem.isAttached()) - return false; - - memLock.acquire(); - const bool isRunning = sharedMem.attach(); - - if (isRunning) - sharedMem.detach(); - - memLock.release(); - - return isRunning; -} - -bool -RunGuard::tryToRun() -{ - // Extra check - if (isAnotherRunning()) - return false; - - memLock.acquire(); - const bool result = sharedMem.create(sizeof(quint64)); - memLock.release(); - - if (!result) { - release(); - return false; - } - - return true; -} - -void -RunGuard::release() -{ - memLock.acquire(); - - if (sharedMem.isAttached()) - sharedMem.detach(); - - memLock.release(); -} diff --git a/src/RunGuard.h b/src/RunGuard.h deleted file mode 100644 index f9a9641a..00000000 --- a/src/RunGuard.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -// -// Taken from -// https://stackoverflow.com/questions/5006547/qt-best-practice-for-a-single-instance-app-protection -// - -#include <QObject> -#include <QSharedMemory> -#include <QSystemSemaphore> - -class RunGuard -{ -public: - RunGuard(const QString &key); - ~RunGuard(); - - bool isAnotherRunning(); - bool tryToRun(); - void release(); - -private: - const QString key; - const QString memLockKey; - const QString sharedmemKey; - - QSharedMemory sharedMem; - QSystemSemaphore memLock; - - Q_DISABLE_COPY(RunGuard) -}; diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp index 2f447cd8..4934ec05 100644 --- a/src/SideBarActions.cpp +++ b/src/SideBarActions.cpp @@ -1,15 +1,15 @@ -#include <QDebug> #include <QIcon> +#include <QPainter> +#include <QResizeEvent> #include <mtx/requests.hpp> #include "Config.h" #include "MainWindow.h" #include "SideBarActions.h" -#include "Utils.h" +#include "Splitter.h" #include "ui/FlatButton.h" #include "ui/Menu.h" -#include "ui/OverlayModal.h" SideBarActions::SideBarActions(QWidget *parent) : QWidget{parent} @@ -93,7 +93,7 @@ SideBarActions::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() <= sidebarSizes.small) { roomDirectory_->hide(); diff --git a/src/SideBarActions.h b/src/SideBarActions.h index ce96cba8..662750b3 100644 --- a/src/SideBarActions.h +++ b/src/SideBarActions.h @@ -2,7 +2,6 @@ #include <QAction> #include <QHBoxLayout> -#include <QResizeEvent> #include <QWidget> namespace mtx { @@ -13,6 +12,7 @@ struct CreateRoom; class Menu; class FlatButton; +class QResizeEvent; class SideBarActions : public QWidget { diff --git a/src/Splitter.cpp b/src/Splitter.cpp index ddb1dc1c..04375853 100644 --- a/src/Splitter.cpp +++ b/src/Splitter.cpp @@ -15,24 +15,19 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QApplication> -#include <QDebug> -#include <QDesktopWidget> #include <QSettings> -#include <QShortcut> -#include "Config.h" +#include "Logging.h" #include "Splitter.h" constexpr auto MaxWidth = (1 << 24) - 1; Splitter::Splitter(QWidget *parent) : QSplitter(parent) - , sz_{utils::calculateSidebarSizes(QFont{})} + , sz_{splitter::calculateSidebarSizes(QFont{})} { connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved); setChildrenCollapsible(false); - setStyleSheet("QSplitter::handle { image: none; }"); } void @@ -80,7 +75,7 @@ Splitter::onSplitterMoved(int pos, int index) auto s = sizes(); if (s.count() < 2) { - qWarning() << "Splitter needs at least two children"; + nhlog::ui()->warn("Splitter needs at least two children"); return; } @@ -165,3 +160,17 @@ Splitter::showFullRoomList() left->show(); left->setMaximumWidth(MaxWidth); } + +splitter::SideBarSizes +splitter::calculateSidebarSizes(const QFont &f) +{ + const auto height = static_cast<double>(QFontMetrics{f}.lineSpacing()); + + SideBarSizes sz; + sz.small = std::ceil(3.8 * height); + sz.normal = std::ceil(16 * height); + sz.groups = std::ceil(3 * height); + sz.collapsePoint = 2 * sz.normal; + + return sz; +} diff --git a/src/Splitter.h b/src/Splitter.h index 14d6773e..7bde89de 100644 --- a/src/Splitter.h +++ b/src/Splitter.h @@ -17,15 +17,27 @@ #pragma once -#include "Utils.h" #include <QSplitter> +namespace splitter { +struct SideBarSizes +{ + int small; + int normal; + int groups; + int collapsePoint; +}; + +SideBarSizes +calculateSidebarSizes(const QFont &f); +} + class Splitter : public QSplitter { Q_OBJECT public: explicit Splitter(QWidget *parent = nullptr); - ~Splitter(); + ~Splitter() override; void restoreSizes(int fallback); @@ -45,5 +57,5 @@ private: int leftMoveCount_ = 0; int rightMoveCount_ = 0; - utils::SideBarSizes sz_; + splitter::SideBarSizes sz_; }; diff --git a/src/SuggestionsPopup.cpp b/src/SuggestionsPopup.cpp deleted file mode 100644 index 952d2ef3..00000000 --- a/src/SuggestionsPopup.cpp +++ /dev/null @@ -1,296 +0,0 @@ -#include <QPaintEvent> -#include <QPainter> -#include <QStyleOption> - -#include "Config.h" -#include "SuggestionsPopup.h" -#include "Utils.h" -#include "ui/Avatar.h" -#include "ui/DropShadow.h" - -constexpr int PopupHMargin = 4; -constexpr int PopupItemMargin = 3; - -PopupItem::PopupItem(QWidget *parent) - : QWidget(parent) - , avatar_{new Avatar(this)} - , hovering_{false} -{ - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setContentsMargins( - PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin); - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); -} - -void -PopupItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - if (underMouse() || hovering_) - p.fillRect(rect(), hoverColor_); -} - -UserItem::UserItem(QWidget *parent, const QString &user_id) - : PopupItem(parent) - , userId_{user_id} -{ - auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); - - avatar_->setSize(conf::popup::avatar); - avatar_->setLetter(utils::firstChar(displayName)); - - // If it's a matrix id we use the second letter. - if (displayName.size() > 1 && displayName.at(0) == '@') - avatar_->setLetter(QChar(displayName.at(1))); - - userName_ = new QLabel(displayName, this); - - topLayout_->addWidget(avatar_); - topLayout_->addWidget(userName_, 1); - - resolveAvatar(user_id); -} - -void -UserItem::updateItem(const QString &user_id) -{ - userId_ = user_id; - - auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); - - // If it's a matrix id we use the second letter. - if (displayName.size() > 1 && displayName.at(0) == '@') - avatar_->setLetter(QChar(displayName.at(1))); - else - avatar_->setLetter(utils::firstChar(displayName)); - - userName_->setText(displayName); - resolveAvatar(user_id); -} - -void -UserItem::resolveAvatar(const QString &user_id) -{ - AvatarProvider::resolve( - ChatPage::instance()->currentRoom(), userId_, this, [this, user_id](const QImage &img) { - // The user on the widget when the avatar is resolved, - // might be different from the user that made the call. - if (user_id == userId_) - avatar_->setImage(img); - else - // We try to resolve the avatar again. - resolveAvatar(userId_); - }); -} - -void -UserItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() != Qt::RightButton) - emit clicked( - Cache::displayName(ChatPage::instance()->currentRoom(), selectedText())); - - QWidget::mousePressEvent(event); -} - -RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) - : PopupItem(parent) - , roomId_{QString::fromStdString(res.room_id)} -{ - auto name = QFontMetrics(QFont()).elidedText( - QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); - - avatar_->setSize(conf::popup::avatar + 6); - avatar_->setLetter(utils::firstChar(name)); - - roomName_ = new QLabel(name, this); - roomName_->setMargin(0); - - topLayout_->addWidget(avatar_); - topLayout_->addWidget(roomName_, 1); - - if (!res.img.isNull()) - avatar_->setImage(res.img); -} - -void -RoomItem::updateItem(const RoomSearchResult &result) -{ - roomId_ = QString::fromStdString(std::move(result.room_id)); - - auto name = - QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)), - Qt::ElideRight, - parentWidget()->width() - 10); - - roomName_->setText(name); - - if (!result.img.isNull()) - avatar_->setImage(result.img); - else - avatar_->setLetter(utils::firstChar(name)); -} - -void -RoomItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() != Qt::RightButton) - emit clicked(selectedText()); - - QWidget::mousePressEvent(event); -} - -SuggestionsPopup::SuggestionsPopup(QWidget *parent) - : QWidget(parent) -{ - setAttribute(Qt::WA_ShowWithoutActivating, true); - setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); - - layout_ = new QVBoxLayout(this); - layout_->setMargin(0); - layout_->setSpacing(0); -} - -void -SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms) -{ - if (rooms.empty()) { - hide(); - return; - } - - const size_t layoutCount = layout_->count(); - const size_t roomCount = rooms.size(); - - // Remove the extra widgets from the layout. - if (roomCount < layoutCount) - removeLayoutItemsAfter(roomCount - 1); - - for (size_t i = 0; i < roomCount; ++i) { - auto item = layout_->itemAt(i); - - // Create a new widget if there isn't already one in that - // layout position. - if (!item) { - auto room = new RoomItem(this, rooms.at(i)); - connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected); - layout_->addWidget(room); - } else { - // Update the current widget with the new data. - auto room = qobject_cast<RoomItem *>(item->widget()); - if (room) - room->updateItem(rooms.at(i)); - } - } - - resetSelection(); - adjustSize(); - - resize(geometry().width(), 40 * rooms.size()); - - selectNextSuggestion(); -} - -void -SuggestionsPopup::addUsers(const QVector<SearchResult> &users) -{ - if (users.isEmpty()) { - hide(); - return; - } - - const size_t layoutCount = layout_->count(); - const size_t userCount = users.size(); - - // Remove the extra widgets from the layout. - if (userCount < layoutCount) - removeLayoutItemsAfter(userCount - 1); - - for (size_t i = 0; i < userCount; ++i) { - auto item = layout_->itemAt(i); - - // Create a new widget if there isn't already one in that - // layout position. - if (!item) { - auto user = new UserItem(this, users.at(i).user_id); - connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected); - layout_->addWidget(user); - } else { - // Update the current widget with the new data. - auto userWidget = qobject_cast<UserItem *>(item->widget()); - if (userWidget) - userWidget->updateItem(users.at(i).user_id); - } - } - - resetSelection(); - adjustSize(); - - selectNextSuggestion(); -} - -void -SuggestionsPopup::hoverSelection() -{ - resetHovering(); - setHovering(selectedItem_); - update(); -} - -void -SuggestionsPopup::selectNextSuggestion() -{ - selectedItem_++; - if (selectedItem_ >= layout_->count()) - selectFirstItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::selectPreviousSuggestion() -{ - selectedItem_--; - if (selectedItem_ < 0) - selectLastItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::resetHovering() -{ - for (int i = 0; i < layout_->count(); ++i) { - const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget()); - - if (item) - item->setHovering(false); - } -} - -void -SuggestionsPopup::setHovering(int pos) -{ - const auto &item = layout_->itemAt(pos); - const auto &widget = qobject_cast<PopupItem *>(item->widget()); - - if (widget) - widget->setHovering(true); -} - -void -SuggestionsPopup::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/SuggestionsPopup.h b/src/SuggestionsPopup.h deleted file mode 100644 index 72d6c7eb..00000000 --- a/src/SuggestionsPopup.h +++ /dev/null @@ -1,147 +0,0 @@ -#pragma once - -#include <QHBoxLayout> -#include <QLabel> -#include <QPoint> -#include <QWidget> - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" - -class Avatar; -struct SearchResult; - -class PopupItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor) - Q_PROPERTY(bool hovering READ hovering WRITE setHovering) - -public: - PopupItem(QWidget *parent); - - QString selectedText() const { return QString(); } - QColor hoverColor() const { return hoverColor_; } - void setHoverColor(QColor &color) { hoverColor_ = color; } - - bool hovering() const { return hovering_; } - void setHovering(const bool hover) { hovering_ = hover; }; - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void clicked(const QString &text); - -protected: - QHBoxLayout *topLayout_; - Avatar *avatar_; - QColor hoverColor_; - - //! Set if the item is currently being - //! hovered during tab completion (cycling). - bool hovering_; -}; - -class UserItem : public PopupItem -{ - Q_OBJECT - -public: - UserItem(QWidget *parent, const QString &user_id); - QString selectedText() const { return userId_; } - void updateItem(const QString &user_id); - -protected: - void mousePressEvent(QMouseEvent *event) override; - -private: - void resolveAvatar(const QString &user_id); - - QLabel *userName_; - QString userId_; -}; - -class RoomItem : public PopupItem -{ - Q_OBJECT - -public: - RoomItem(QWidget *parent, const RoomSearchResult &res); - QString selectedText() const { return roomId_; } - void updateItem(const RoomSearchResult &res); - -protected: - void mousePressEvent(QMouseEvent *event) override; - -private: - QLabel *roomName_; - QString roomId_; - RoomSearchResult info_; -}; - -class SuggestionsPopup : public QWidget -{ - Q_OBJECT - -public: - explicit SuggestionsPopup(QWidget *parent = nullptr); - - template<class Item> - void selectHoveredSuggestion() - { - const auto item = layout_->itemAt(selectedItem_); - if (!item) - return; - - const auto &widget = qobject_cast<Item *>(item->widget()); - emit itemSelected( - Cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); - - resetSelection(); - } - -public slots: - void addUsers(const QVector<SearchResult> &users); - void addRooms(const std::vector<RoomSearchResult> &rooms); - - //! Move to the next available suggestion item. - void selectNextSuggestion(); - //! Move to the previous available suggestion item. - void selectPreviousSuggestion(); - //! Remove hovering from all items. - void resetHovering(); - //! Set hovering to the item in the given layout position. - void setHovering(int pos); - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void itemSelected(const QString &user); - -private: - void hoverSelection(); - void resetSelection() { selectedItem_ = -1; } - void selectFirstItem() { selectedItem_ = 0; } - void selectLastItem() { selectedItem_ = layout_->count() - 1; } - void removeLayoutItemsAfter(size_t startingPos) - { - size_t posToRemove = layout_->count() - 1; - - QLayoutItem *item; - while (startingPos <= posToRemove && (item = layout_->takeAt(posToRemove)) != 0) { - delete item->widget(); - delete item; - - posToRemove = layout_->count() - 1; - } - } - - QVBoxLayout *layout_; - - //! Counter for tab completion (cycling). - int selectedItem_ = -1; -}; diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 934f2b2c..11f7ddda 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -16,12 +16,9 @@ */ #include <QAbstractTextDocumentLayout> -#include <QApplication> #include <QBuffer> #include <QClipboard> -#include <QDebug> #include <QFileDialog> -#include <QImageReader> #include <QMimeData> #include <QMimeDatabase> #include <QMimeType> @@ -31,7 +28,7 @@ #include "Cache.h" #include "ChatPage.h" -#include "Config.h" +#include "Logging.h" #include "TextInputWidget.h" #include "Utils.h" #include "ui/FlatButton.h" @@ -48,7 +45,8 @@ static constexpr int ButtonHeight = 22; FilteredTextEdit::FilteredTextEdit(QWidget *parent) : QTextEdit{parent} , history_index_{0} - , popup_{parent} + , suggestionsPopup_{parent} + , replyPopup_{parent} , previewDialog_{parent} { setFrameStyle(QFrame::NoFrame); @@ -75,36 +73,43 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) &FilteredTextEdit::uploadData); connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); - connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { - popup_.hide(); + connect(&replyPopup_, &ReplyPopup::userSelected, this, [](const QString &text) { + // TODO: Show user avatar window. + nhlog::ui()->info("User selected: " + text.toStdString()); + }); + connect( + &suggestionsPopup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { + suggestionsPopup_.hide(); - auto cursor = textCursor(); - const int end = cursor.position(); + auto cursor = textCursor(); + const int end = cursor.position(); - cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); - cursor.setPosition(end, QTextCursor::KeepAnchor); - cursor.removeSelectedText(); - cursor.insertText(text); - }); + cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); + cursor.setPosition(end, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + cursor.insertText(text); + }); + + connect(&replyPopup_, &ReplyPopup::cancel, this, [this]() { closeReply(); }); // For cycling through the suggestions by hitting tab. connect(this, &FilteredTextEdit::selectNextSuggestion, - &popup_, + &suggestionsPopup_, &SuggestionsPopup::selectNextSuggestion); connect(this, &FilteredTextEdit::selectPreviousSuggestion, - &popup_, + &suggestionsPopup_, &SuggestionsPopup::selectPreviousSuggestion); connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { - popup_.selectHoveredSuggestion<UserItem>(); + suggestionsPopup_.selectHoveredSuggestion<UserItem>(); }); previewDialog_.hide(); } void -FilteredTextEdit::showResults(const QVector<SearchResult> &results) +FilteredTextEdit::showResults(const std::vector<SearchResult> &results) { QPoint pos; @@ -117,9 +122,9 @@ FilteredTextEdit::showResults(const QVector<SearchResult> &results) pos = viewport()->mapToGlobal(rect.topLeft()); } - popup_.addUsers(results); - popup_.move(pos.x(), pos.y() - popup_.height() - 10); - popup_.show(); + suggestionsPopup_.addUsers(results); + suggestionsPopup_.move(pos.x(), pos.y() - suggestionsPopup_.height() - 10); + suggestionsPopup_.show(); } void @@ -146,7 +151,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) closeSuggestions(); } - if (popup_.isVisible()) { + if (suggestionsPopup_.isVisible()) { switch (event->key()) { case Qt::Key_Down: case Qt::Key_Tab: @@ -169,6 +174,17 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } + if (replyPopup_.isVisible()) { + switch (event->key()) { + case Qt::Key_Escape: + closeReply(); + return; + + default: + break; + } + } + switch (event->key()) { case Qt::Key_At: atTriggerPosition_ = textCursor().position(); @@ -202,6 +218,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) if (!(event->modifiers() & Qt::ShiftModifier)) { stopTyping(); submit(); + closeReply(); } else { QTextEdit::keyPressEvent(event); } @@ -286,8 +303,9 @@ FilteredTextEdit::insertFromMimeData(const QMimeData *source) const auto audio = formats.filter("audio/", Qt::CaseInsensitive); const auto video = formats.filter("video/", Qt::CaseInsensitive); - if (!image.empty()) { - showPreview(source, image); + if (source->hasImage()) { + QImage img = qvariant_cast<QImage>(source->imageData()); + previewDialog_.setPreview(img, image.front()); } else if (!audio.empty()) { showPreview(source, audio); } else if (!video.empty()) { @@ -398,39 +416,49 @@ FilteredTextEdit::submit() auto name = text.mid(1, command_end - 1); auto args = text.mid(command_end + 1); if (name.isEmpty() || name == "/") { - message(args); + message(args, related); } else { command(name, args); } } else { - message(std::move(text)); + message(std::move(text), std::move(related)); } + related = {}; + clear(); } void +FilteredTextEdit::showReplyPopup(const RelatedInfo &related_) +{ + QPoint pos = viewport()->mapToGlobal(this->pos()); + + replyPopup_.setReplyContent(related_); + replyPopup_.move(pos.x(), pos.y() - replyPopup_.height() - 10); + replyPopup_.setFixedWidth(this->parentWidget()->width()); + replyPopup_.show(); +} + +void FilteredTextEdit::textChanged() { working_history_[history_index_] = toPlainText(); } void -FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename) +FilteredTextEdit::uploadData(const QByteArray data, + const QString &mediaType, + const QString &filename) { QSharedPointer<QBuffer> buffer{new QBuffer{this}}; buffer->setData(data); emit startedUpload(); - if (media == "image") - emit image(buffer, filename); - else if (media == "audio") - emit audio(buffer, filename); - else if (media == "video") - emit video(buffer, filename); - else - emit file(buffer, filename); + emit media(buffer, mediaType, filename, related); + related = {}; + closeReply(); } void @@ -492,15 +520,15 @@ TextInputWidget::TextInputWidget(QWidget *parent) emit heightChanged(widgetHeight); }); connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { - if (q.isEmpty() || !cache::client()) + if (q.isEmpty()) return; QtConcurrent::run([this, q = q.toLower().toStdString()]() { try { - emit input_->resultsRetrieved(cache::client()->searchUsers( + emit input_->resultsRetrieved(cache::searchUsers( ChatPage::instance()->currentRoom().toStdString(), q)); } catch (const lmdb::error &e) { - std::cout << e.what() << '\n'; + nhlog::db()->error("Suggestion retrieval failed: {}", e.what()); } }); }); @@ -537,10 +565,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); - connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage); - connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio); - connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo); - connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile); + connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -574,21 +599,36 @@ void TextInputWidget::command(QString command, QString args) { if (command == "me") { - sendEmoteMessage(args); + sendEmoteMessage(args, input_->related); } else if (command == "join") { sendJoinRoomRequest(args); + } else if (command == "invite") { + sendInviteRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "kick") { + sendKickRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "ban") { + sendBanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "unban") { + sendUnbanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); } else if (command == "shrug") { - sendTextMessage("¯\\_(ツ)_/¯"); + sendTextMessage("¯\\_(ツ)_/¯", input_->related); } else if (command == "fliptable") { - sendTextMessage("(╯°□°)╯︵ ┻━┻"); + sendTextMessage("(╯°□°)╯︵ ┻━┻", input_->related); + } else if (command == "unfliptable") { + sendTextMessage(" ┯━┯╭( º _ º╭)", input_->related); + } else if (command == "sovietflip") { + sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\", input_->related); } + + input_->related = std::nullopt; } void TextInputWidget::openFileSelection() { + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); const auto fileName = - QFileDialog::getOpenFileName(this, tr("Select a file"), "", tr("All Files (*)")); + QFileDialog::getOpenFileName(this, tr("Select a file"), homeFolder, tr("All Files (*)")); if (fileName.isEmpty()) return; @@ -599,14 +639,10 @@ TextInputWidget::openFileSelection() const auto format = mime.name().split("/")[0]; QSharedPointer<QFile> file{new QFile{fileName, this}}; - if (format == "image") - emit uploadImage(file, fileName); - else if (format == "audio") - emit uploadAudio(file, fileName); - else if (format == "video") - emit uploadVideo(file, fileName); - else - emit uploadFile(file, fileName); + + emit uploadMedia(file, format, QFileInfo(fileName).fileName(), input_->related); + input_->related = {}; + input_->closeReply(); showUploadSpinner(); } @@ -653,12 +689,14 @@ TextInputWidget::paintEvent(QPaintEvent *) } void -TextInputWidget::addReply(const QString &username, const QString &msg) +TextInputWidget::addReply(const RelatedInfo &related) { - input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); + // input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); input_->setFocus(); + // input_->showReplyPopup(related); auto cursor = input_->textCursor(); cursor.movePosition(QTextCursor::End); input_->setTextCursor(cursor); + input_->related = related; } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 8f634f6b..77d77e44 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -18,23 +18,18 @@ #pragma once #include <deque> -#include <iterator> -#include <map> +#include <optional> -#include <QApplication> -#include <QDebug> +#include <QCoreApplication> #include <QHBoxLayout> #include <QPaintEvent> #include <QTextEdit> #include <QWidget> -#include "SuggestionsPopup.h" #include "dialogs/PreviewUploadOverlay.h" #include "emoji/PickButton.h" - -namespace dialogs { -class PreviewUploadOverlay; -} +#include "popups/ReplyPopup.h" +#include "popups/SuggestionsPopup.h" struct SearchResult; @@ -54,28 +49,37 @@ public: QSize minimumSizeHint() const override; void submit(); + void showReplyPopup(const RelatedInfo &related_); + void closeReply() + { + replyPopup_.hide(); + related = {}; + } + + // Used for replies + std::optional<RelatedInfo> related; signals: void heightChanged(int height); void startedTyping(); void stoppedTyping(); void startedUpload(); - void message(QString); + void message(QString, const std::optional<RelatedInfo> &); void command(QString name, QString args); - void image(QSharedPointer<QIODevice> data, const QString &filename); - void audio(QSharedPointer<QIODevice> data, const QString &filename); - void video(QSharedPointer<QIODevice> data, const QString &filename); - void file(QSharedPointer<QIODevice> data, const QString &filename); + void media(QSharedPointer<QIODevice> data, + QString mimeClass, + const QString &filename, + const std::optional<RelatedInfo> &related); //! Trigger the suggestion popup. void showSuggestions(const QString &query); - void resultsRetrieved(const QVector<SearchResult> &results); + void resultsRetrieved(const std::vector<SearchResult> &results); void selectNextSuggestion(); void selectPreviousSuggestion(); void selectHoveredSuggestion(); public slots: - void showResults(const QVector<SearchResult> &results); + void showResults(const std::vector<SearchResult> &results); protected: void keyPressEvent(QKeyEvent *event) override; @@ -83,7 +87,7 @@ protected: void insertFromMimeData(const QMimeData *source) override; void focusOutEvent(QFocusEvent *event) override { - popup_.hide(); + suggestionsPopup_.hide(); QTextEdit::focusOutEvent(event); } @@ -92,7 +96,8 @@ private: size_t history_index_; QTimer *typingTimer_; - SuggestionsPopup popup_; + SuggestionsPopup suggestionsPopup_; + ReplyPopup replyPopup_; enum class AnchorType { @@ -104,7 +109,7 @@ private: int anchorWidth(AnchorType anchor) { return static_cast<int>(anchor); } - void closeSuggestions() { popup_.hide(); } + void closeSuggestions() { suggestionsPopup_.hide(); } void resetAnchor() { atTriggerPosition_ = -1; } bool isAnchorValid() { return atTriggerPosition_ != -1; } bool hasAnchor(int pos, AnchorType anchor) @@ -137,7 +142,7 @@ class TextInputWidget : public QWidget Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) public: - TextInputWidget(QWidget *parent = 0); + TextInputWidget(QWidget *parent = nullptr); void stopTyping(); @@ -158,22 +163,27 @@ public slots: void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } - void addReply(const QString &username, const QString &msg); + void addReply(const RelatedInfo &related); + void closeReplyPopup() { input_->closeReply(); } private slots: void addSelectedEmoji(const QString &emoji); signals: - void sendTextMessage(QString msg); - void sendEmoteMessage(QString msg); + void sendTextMessage(const QString &msg, const std::optional<RelatedInfo> &related); + void sendEmoteMessage(QString msg, const std::optional<RelatedInfo> &related); void heightChanged(int height); - void uploadImage(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadFile(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadAudio(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadVideo(const QSharedPointer<QIODevice> data, const QString &filename); + void uploadMedia(const QSharedPointer<QIODevice> data, + QString mimeClass, + const QString &filename, + const std::optional<RelatedInfo> &related); void sendJoinRoomRequest(const QString &room); + void sendInviteRoomRequest(const QString &userid, const QString &reason); + void sendKickRoomRequest(const QString &userid, const QString &reason); + void sendBanRoomRequest(const QString &userid, const QString &reason); + void sendUnbanRoomRequest(const QString &userid, const QString &reason); void startedTyping(); void stoppedTyping(); diff --git a/src/TopRoomBar.cpp b/src/TopRoomBar.cpp index 5c817dc2..ffd57d50 100644 --- a/src/TopRoomBar.cpp +++ b/src/TopRoomBar.cpp @@ -15,8 +15,16 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QDebug> +#include <QAction> +#include <QIcon> +#include <QLabel> +#include <QPaintEvent> +#include <QPainter> +#include <QPen> +#include <QPoint> +#include <QStyle> #include <QStyleOption> +#include <QVBoxLayout> #include "Config.h" #include "MainWindow.h" @@ -46,9 +54,8 @@ TopRoomBar::TopRoomBar(QWidget *parent) topLayout_->setContentsMargins( 2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, fontHeight * 2); avatar_->setLetter(""); - avatar_->setSize(fontHeight * 2); textLayout_ = new QVBoxLayout(); textLayout_->setSpacing(0); @@ -80,11 +87,21 @@ TopRoomBar::TopRoomBar(QWidget *parent) settingsBtn_->setFixedSize(buttonSize_, buttonSize_); settingsBtn_->setCornerRadius(buttonSize_ / 2); + mentionsBtn_ = new FlatButton(this); + mentionsBtn_->setToolTip(tr("Mentions")); + mentionsBtn_->setFixedSize(buttonSize_, buttonSize_); + mentionsBtn_->setCornerRadius(buttonSize_ / 2); + QIcon settings_icon; settings_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); settingsBtn_->setIcon(settings_icon); settingsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); + QIcon mentions_icon; + mentions_icon.addFile(":/icons/icons/ui/at-solid.svg"); + mentionsBtn_->setIcon(mentions_icon); + mentionsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); + backBtn_ = new FlatButton(this); backBtn_->setFixedSize(buttonSize_, buttonSize_); backBtn_->setCornerRadius(buttonSize_ / 2); @@ -100,6 +117,7 @@ TopRoomBar::TopRoomBar(QWidget *parent) topLayout_->addWidget(avatar_); topLayout_->addWidget(backBtn_); topLayout_->addLayout(textLayout_, 1); + topLayout_->addWidget(mentionsBtn_, 0, Qt::AlignRight); topLayout_->addWidget(settingsBtn_, 0, Qt::AlignRight); menu_ = new Menu(this); @@ -135,6 +153,11 @@ TopRoomBar::TopRoomBar(QWidget *parent) menu_->popup( QPoint(pos.x() + buttonSize_ - menu_->sizeHint().width(), pos.y() + buttonSize_)); }); + + connect(mentionsBtn_, &QPushButton::clicked, this, [this]() { + auto pos = mapToGlobal(mentionsBtn_->pos()); + emit mentionsClicked(pos); + }); } void @@ -167,7 +190,7 @@ TopRoomBar::reset() } void -TopRoomBar::updateRoomAvatar(const QImage &avatar_image) +TopRoomBar::updateRoomAvatar(const QString &avatar_image) { avatar_->setImage(avatar_image); update(); @@ -195,3 +218,19 @@ TopRoomBar::updateRoomTopic(QString topic) topicLabel_->setHtml(topic); update(); } + +void +TopRoomBar::mousePressEvent(QMouseEvent *) +{ + if (roomSettings_ != nullptr) + roomSettings_->trigger(); +} + +void +TopRoomBar::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h index 5b7d3344..63ce847e 100644 --- a/src/TopRoomBar.h +++ b/src/TopRoomBar.h @@ -17,16 +17,9 @@ #pragma once -#include <QAction> -#include <QIcon> -#include <QImage> -#include <QLabel> -#include <QPaintEvent> -#include <QPainter> -#include <QPen> -#include <QStyle> -#include <QStyleOption> -#include <QVBoxLayout> +#include <QColor> +#include <QStringList> +#include <QWidget> class Avatar; class FlatButton; @@ -34,6 +27,12 @@ class Menu; class TextLabel; class OverlayModal; +class QPainter; +class QLabel; +class QIcon; +class QHBoxLayout; +class QVBoxLayout; + class TopRoomBar : public QWidget { Q_OBJECT @@ -41,9 +40,9 @@ class TopRoomBar : public QWidget Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) public: - TopRoomBar(QWidget *parent = 0); + TopRoomBar(QWidget *parent = nullptr); - void updateRoomAvatar(const QImage &avatar_image); + void updateRoomAvatar(const QString &avatar_image); void updateRoomAvatar(const QIcon &icon); void updateRoomName(const QString &name); void updateRoomTopic(QString topic); @@ -63,21 +62,11 @@ public slots: signals: void inviteUsers(QStringList users); void showRoomList(); + void mentionsClicked(const QPoint &pos); protected: - void mousePressEvent(QMouseEvent *) override - { - if (roomSettings_ != nullptr) - roomSettings_->trigger(); - } - - void paintEvent(QPaintEvent *) override - { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - } + void mousePressEvent(QMouseEvent *) override; + void paintEvent(QPaintEvent *) override; private: QHBoxLayout *topLayout_ = nullptr; @@ -93,6 +82,7 @@ private: QAction *inviteUsers_ = nullptr; FlatButton *settingsBtn_; + FlatButton *mentionsBtn_; FlatButton *backBtn_; Avatar *avatar_; diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp index e7348b89..6ab011d1 100644 --- a/src/TrayIcon.cpp +++ b/src/TrayIcon.cpp @@ -15,9 +15,11 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QAction> #include <QApplication> #include <QList> #include <QMenu> +#include <QPainter> #include <QTimer> #include "TrayIcon.h" @@ -134,12 +136,16 @@ TrayIcon::setUnreadCount(int count) { // Use the native badge counter in MacOS. #if defined(Q_OS_MAC) +// currently, to avoid writing obj-c code, ignore deprecated warnings on the badge functions +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" auto labelText = count == 0 ? "" : QString::number(count); if (labelText == QtMac::badgeLabelText()) return; QtMac::setBadgeLabelText(labelText); +#pragma clang diagnostic pop #elif defined(Q_OS_WIN) // FIXME: Find a way to use Windows apis for the badge counter (if any). #else diff --git a/src/TrayIcon.h b/src/TrayIcon.h index a3536cc3..24ac81da 100644 --- a/src/TrayIcon.h +++ b/src/TrayIcon.h @@ -17,22 +17,23 @@ #pragma once -#include <QAction> #include <QIcon> #include <QIconEngine> -#include <QPainter> #include <QRect> #include <QSystemTrayIcon> +class QAction; +class QPainter; + class MsgCountComposedIcon : public QIconEngine { public: MsgCountComposedIcon(const QString &filename); - virtual void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state); - virtual QIconEngine *clone() const; - virtual QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const; - virtual QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state); + void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + QIconEngine *clone() const override; + QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; int msgCount = 0; diff --git a/src/TypingDisplay.cpp b/src/TypingDisplay.cpp deleted file mode 100644 index 11313adc..00000000 --- a/src/TypingDisplay.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include <QDebug> -#include <QPainter> -#include <QPoint> -#include <QShowEvent> - -#include "Config.h" -#include "TypingDisplay.h" -#include "ui/Painter.h" - -constexpr int LEFT_PADDING = 24; -constexpr int RECT_PADDING = 2; - -TypingDisplay::TypingDisplay(QWidget *parent) - : OverlayWidget(parent) - , offset_{conf::textInput::height} -{ - setFixedHeight(QFontMetrics(font()).height() + RECT_PADDING); - setAttribute(Qt::WA_TransparentForMouseEvents); -} - -void -TypingDisplay::setOffset(int margin) -{ - offset_ = margin; - move(0, parentWidget()->height() - offset_ - height()); -} - -void -TypingDisplay::setUsers(const QStringList &uid) -{ - move(0, parentWidget()->height() - offset_ - height()); - - text_.clear(); - - if (uid.isEmpty()) { - hide(); - update(); - - return; - } - - text_ = uid.join(", "); - - if (uid.size() == 1) - text_ += tr(" is typing"); - else if (uid.size() > 1) - text_ += tr(" are typing"); - - show(); - update(); -} - -void -TypingDisplay::paintEvent(QPaintEvent *) -{ - Painter p(this); - PainterHighQualityEnabler hq(p); - - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - - p.setFont(f); - p.setPen(QPen(textColor())); - - QRect region = rect(); - region.translate(LEFT_PADDING, 0); - - QFontMetrics fm(f); - text_ = fm.elidedText(text_, Qt::ElideRight, (double)(width() * 0.75)); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, fm.width(text_) + 2 * LEFT_PADDING, height()), 3, 3); - - p.fillPath(path, backgroundColor()); - p.drawText(region, Qt::AlignVCenter, text_); -} diff --git a/src/TypingDisplay.h b/src/TypingDisplay.h deleted file mode 100644 index 332d9c66..00000000 --- a/src/TypingDisplay.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "ui/OverlayWidget.h" - -class QPaintEvent; - -class TypingDisplay : public OverlayWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - TypingDisplay(QWidget *parent = nullptr); - - void setUsers(const QStringList &user_ids); - - void setTextColor(const QColor &color) { textColor_ = color; }; - QColor textColor() const { return textColor_; }; - - void setBackgroundColor(const QColor &color) { bgColor_ = color; }; - QColor backgroundColor() const { return bgColor_; }; - -public slots: - void setOffset(int margin); - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - int offset_; - QColor textColor_; - QColor bgColor_; - QString text_; -}; diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp index 5345fb2a..2e21d41f 100644 --- a/src/UserInfoWidget.cpp +++ b/src/UserInfoWidget.cpp @@ -1,3 +1,4 @@ + /* * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> * @@ -15,14 +16,15 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QPainter> #include <QTimer> #include <iostream> #include "Config.h" #include "MainWindow.h" +#include "Splitter.h" #include "UserInfoWidget.h" -#include "Utils.h" #include "ui/Avatar.h" #include "ui/FlatButton.h" #include "ui/OverlayModal.h" @@ -52,10 +54,9 @@ UserInfoWidget::UserInfoWidget(QWidget *parent) textLayout_->setSpacing(widgetMargin / 2); textLayout_->setContentsMargins(widgetMargin * 2, widgetMargin, widgetMargin, widgetMargin); - userAvatar_ = new Avatar(this); + userAvatar_ = new Avatar(this, fontHeight * 2.5); userAvatar_->setObjectName("userAvatar"); userAvatar_->setLetter(QChar('?')); - userAvatar_->setSize(fontHeight * 2.5); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); @@ -108,7 +109,7 @@ UserInfoWidget::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); if (width() <= sz.small) { topLayout_->setContentsMargins(0, 0, logoutButtonSize_, 0); @@ -135,14 +136,6 @@ UserInfoWidget::reset() } void -UserInfoWidget::setAvatar(const QImage &img) -{ - avatar_image_ = img; - userAvatar_->setImage(img); - update(); -} - -void UserInfoWidget::setDisplayName(const QString &name) { if (name.isEmpty()) @@ -160,6 +153,14 @@ UserInfoWidget::setUserId(const QString &userid) { user_id_ = userid; userIdLabel_->setText(userid); + update(); +} + +void +UserInfoWidget::setAvatar(const QString &url) +{ + userAvatar_->setImage(url); + update(); } void diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h index 65de7be9..e1a925a4 100644 --- a/src/UserInfoWidget.h +++ b/src/UserInfoWidget.h @@ -31,11 +31,11 @@ class UserInfoWidget : public QWidget Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) public: - UserInfoWidget(QWidget *parent = 0); + UserInfoWidget(QWidget *parent = nullptr); - void setAvatar(const QImage &img); void setDisplayName(const QString &name); void setUserId(const QString &userid); + void setAvatar(const QString &url); void reset(); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index e3c0d190..2cac783c 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -18,14 +18,23 @@ #include <QApplication> #include <QComboBox> #include <QFileDialog> +#include <QFormLayout> #include <QInputDialog> #include <QLabel> #include <QLineEdit> #include <QMessageBox> +#include <QPainter> +#include <QProcessEnvironment> #include <QPushButton> +#include <QResizeEvent> #include <QScrollArea> +#include <QScroller> #include <QSettings> +#include <QStandardPaths> +#include <QString> +#include <QTextStream> +#include "Cache.h" #include "Config.h" #include "MatrixClient.h" #include "Olm.h" @@ -46,10 +55,13 @@ UserSettings::load() hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); isStartInTrayEnabled_ = settings.value("user/window/start_in_tray", false).toBool(); isGroupViewEnabled_ = settings.value("user/group_view", true).toBool(); + isMarkdownEnabled_ = settings.value("user/markdown_enabled", true).toBool(); isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool(); isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); - theme_ = settings.value("user/theme", "light").toString(); + theme_ = settings.value("user/theme", defaultTheme_).toString(); font_ = settings.value("user/font_family", "default").toString(); + avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); applyTheme(); @@ -70,6 +82,13 @@ UserSettings::setFontFamily(QString family) } void +UserSettings::setEmojiFontFamily(QString family) +{ + emojiFont_ = family; + save(); +} + +void UserSettings::setTheme(QString theme) { theme_ = theme; @@ -107,13 +126,18 @@ UserSettings::save() settings.setValue("start_in_tray", isStartInTrayEnabled_); settings.endGroup(); + settings.setValue("avatar_circles", avatarCircles_); + settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", isTypingNotificationsEnabled_); settings.setValue("read_receipts", isReadReceiptsEnabled_); settings.setValue("group_view", isGroupViewEnabled_); + settings.setValue("markdown_enabled", isMarkdownEnabled_); settings.setValue("desktop_notifications", hasDesktopNotifications_); settings.setValue("theme", theme()); settings.setValue("font_family", font_); + settings.setValue("emoji_font_family", emojiFont_); + settings.endGroup(); } @@ -128,12 +152,12 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge : QWidget{parent} , settings_{settings} { - topLayout_ = new QVBoxLayout(this); + topLayout_ = new QVBoxLayout{this}; QIcon icon; icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - auto backBtn_ = new FlatButton(this); + auto backBtn_ = new FlatButton{this}; backBtn_->setMinimumSize(QSize(24, 24)); backBtn_->setIcon(icon); backBtn_->setIconSize(QSize(24, 24)); @@ -150,106 +174,65 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); topBarLayout_->addStretch(1); - auto trayOptionLayout_ = new QHBoxLayout; - trayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto trayLabel = new QLabel(tr("Minimize to tray"), this); - trayLabel->setFont(font); - trayToggle_ = new Toggle(this); + formLayout_ = new QFormLayout; - trayOptionLayout_->addWidget(trayLabel); - trayOptionLayout_->addWidget(trayToggle_, 0, Qt::AlignRight); + formLayout_->setLabelAlignment(Qt::AlignLeft); + formLayout_->setFormAlignment(Qt::AlignRight); + formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows); + formLayout_->setHorizontalSpacing(0); + + auto general_ = new QLabel{tr("GENERAL"), this}; + general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + general_->setFont(font); + + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + groupViewToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdownEnabled_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; - auto startInTrayOptionLayout_ = new QHBoxLayout; - startInTrayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto startInTrayLabel = new QLabel(tr("Start in tray"), this); - startInTrayLabel->setFont(font); - startInTrayToggle_ = new Toggle(this); if (!settings_->isTrayEnabled()) startInTrayToggle_->setDisabled(true); - startInTrayOptionLayout_->addWidget(startInTrayLabel); - startInTrayOptionLayout_->addWidget(startInTrayToggle_, 0, Qt::AlignRight); - - auto groupViewLayout = new QHBoxLayout; - groupViewLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto groupViewLabel = new QLabel(tr("Group's sidebar"), this); - groupViewLabel->setFont(font); - groupViewToggle_ = new Toggle(this); - - groupViewLayout->addWidget(groupViewLabel); - groupViewLayout->addWidget(groupViewToggle_, 0, Qt::AlignRight); - - auto typingLayout = new QHBoxLayout; - typingLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto typingLabel = new QLabel(tr("Typing notifications"), this); - typingLabel->setFont(font); - typingNotifications_ = new Toggle(this); - - typingLayout->addWidget(typingLabel); - typingLayout->addWidget(typingNotifications_, 0, Qt::AlignRight); - - auto receiptsLayout = new QHBoxLayout; - receiptsLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto receiptsLabel = new QLabel(tr("Read receipts"), this); - receiptsLabel->setFont(font); - readReceipts_ = new Toggle(this); - - receiptsLayout->addWidget(receiptsLabel); - receiptsLayout->addWidget(readReceipts_, 0, Qt::AlignRight); - - auto desktopLayout = new QHBoxLayout; - desktopLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto desktopLabel = new QLabel(tr("Desktop notifications"), this); - desktopLabel->setFont(font); - desktopNotifications_ = new Toggle(this); - - desktopLayout->addWidget(desktopLabel); - desktopLayout->addWidget(desktopNotifications_, 0, Qt::AlignRight); - - auto scaleFactorOptionLayout = new QHBoxLayout; - scaleFactorOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto scaleFactorLabel = new QLabel(tr("Scale factor"), this); - scaleFactorLabel->setFont(font); - scaleFactorCombo_ = new QComboBox(this); - for (double option = 1; option <= 3; option += 0.25) - scaleFactorCombo_->addItem(QString::number(option)); + avatarCircles_->setFixedSize(64, 48); - scaleFactorOptionLayout->addWidget(scaleFactorLabel); - scaleFactorOptionLayout->addWidget(scaleFactorCombo_, 0, Qt::AlignRight); + auto uiLabel_ = new QLabel{tr("INTERFACE"), this}; + uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin); + uiLabel_->setAlignment(Qt::AlignBottom); + uiLabel_->setFont(font); - auto fontSizeOptionLayout = new QHBoxLayout; - fontSizeOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto fontSizeLabel = new QLabel(tr("Font size"), this); - fontSizeLabel->setFont(font); - fontSizeCombo_ = new QComboBox(this); + for (double option = 1; option <= 3; option += 0.25) + scaleFactorCombo_->addItem(QString::number(option)); for (double option = 10; option < 17; option += 0.5) fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option))); - 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); + // TODO: Is there a way to limit to just emojis, rather than + // all emoji fonts? + auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); + for (const auto &family : emojiFamilies) { + emojiFontSelectionCombo_->addItem(family); + } + + fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(settings_->font())); - fontFamilyOptionLayout->addWidget(fontFamilyLabel); - fontFamilyOptionLayout->addWidget(fontSelectionCombo_, 0, Qt::AlignRight); + emojiFontSelectionCombo_->setCurrentIndex( + emojiFontSelectionCombo_->findText(settings_->emojiFont())); - auto themeOptionLayout_ = new QHBoxLayout; - themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto themeLabel_ = new QLabel(tr("Theme"), this); - themeLabel_->setFont(font); - themeCombo_ = new QComboBox(this); + themeCombo_ = new QComboBox{this}; themeCombo_->addItem("Light"); themeCombo_->addItem("Dark"); themeCombo_->addItem("System"); @@ -259,117 +242,103 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge int themeIndex = themeCombo_->findText(themeStr); themeCombo_->setCurrentIndex(themeIndex); - themeOptionLayout_->addWidget(themeLabel_); - themeOptionLayout_->addWidget(themeCombo_, 0, Qt::AlignRight); - - auto encryptionLayout_ = new QVBoxLayout; - encryptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - encryptionLayout_->setAlignment(Qt::AlignVCenter); + auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; + encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); + encryptionLabel_->setAlignment(Qt::AlignBottom); + encryptionLabel_->setFont(font); QFont monospaceFont; monospaceFont.setFamily("Monospace"); monospaceFont.setStyleHint(QFont::Monospace); monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); - auto deviceIdLayout = new QHBoxLayout; - deviceIdLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - - auto deviceIdLabel = new QLabel(tr("Device ID"), this); - deviceIdLabel->setFont(font); - deviceIdLabel->setMargin(0); deviceIdValue_ = new QLabel{this}; deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); deviceIdValue_->setFont(monospaceFont); - deviceIdLayout->addWidget(deviceIdLabel, 1); - deviceIdLayout->addWidget(deviceIdValue_); - - auto deviceFingerprintLayout = new QHBoxLayout; - deviceFingerprintLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto deviceFingerprintLabel = new QLabel(tr("Device Fingerprint"), this); - deviceFingerprintLabel->setFont(font); - deviceFingerprintLabel->setMargin(0); deviceFingerprintValue_ = new QLabel{this}; deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); deviceFingerprintValue_->setFont(monospaceFont); - deviceFingerprintLayout->addWidget(deviceFingerprintLabel, 1); - deviceFingerprintLayout->addWidget(deviceFingerprintValue_); - auto sessionKeysLayout = new QHBoxLayout; - sessionKeysLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto sessionKeysLabel = new QLabel(tr("Session Keys"), this); + deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); + + auto sessionKeysLabel = new QLabel{tr("Session Keys"), this}; sessionKeysLabel->setFont(font); - sessionKeysLayout->addWidget(sessionKeysLabel, 1); + sessionKeysLabel->setMargin(OptionMargin); auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; - connect( - sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; - connect( - sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); + + auto sessionKeysLayout = new QHBoxLayout; + sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); - encryptionLayout_->addLayout(deviceIdLayout); - encryptionLayout_->addLayout(deviceFingerprintLayout); - encryptionLayout_->addWidget(new HorizontalLine{this}); - encryptionLayout_->addLayout(sessionKeysLayout); - - font.setWeight(QFont::Medium); - - auto encryptionLabel_ = new QLabel(tr("ENCRYPTION"), this); - encryptionLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - encryptionLabel_->setFont(font); - - auto general_ = new QLabel(tr("GENERAL"), this); - general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - general_->setFont(font); - - mainLayout_ = new QVBoxLayout; - mainLayout_->setAlignment(Qt::AlignTop); - mainLayout_->setSpacing(7); - mainLayout_->setContentsMargins( - sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); - mainLayout_->addWidget(general_, 1, Qt::AlignLeft | Qt::AlignBottom); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(trayOptionLayout_); - mainLayout_->addLayout(startInTrayOptionLayout_); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(groupViewLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(typingLayout); - mainLayout_->addLayout(receiptsLayout); - mainLayout_->addLayout(desktopLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - -#if defined(Q_OS_MAC) - scaleFactorLabel->hide(); + auto boxWrap = [this, &font](QString labelText, QWidget *field) { + auto label = new QLabel{labelText, this}; + label->setFont(font); + label->setMargin(OptionMargin); + + auto layout = new QHBoxLayout; + layout->addWidget(field, 0, Qt::AlignRight); + + formLayout_->addRow(label, layout); + }; + + formLayout_->addRow(general_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Minimize to tray"), trayToggle_); + boxWrap(tr("Start in tray"), startInTrayToggle_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Circular Avatars"), avatarCircles_); + boxWrap(tr("Group's sidebar"), groupViewToggle_); + boxWrap(tr("Typing notifications"), typingNotifications_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Read receipts"), readReceipts_); + boxWrap(tr("Send messages as Markdown"), markdownEnabled_); + boxWrap(tr("Desktop notifications"), desktopNotifications_); + formLayout_->addRow(uiLabel_); + formLayout_->addRow(new HorizontalLine{this}); + +#if !defined(Q_OS_MAC) + boxWrap(tr("Scale factor"), scaleFactorCombo_); +#else scaleFactorCombo_->hide(); #endif + boxWrap(tr("Font size"), fontSizeCombo_); + boxWrap(tr("Font Family"), fontSelectionCombo_); - mainLayout_->addLayout(scaleFactorOptionLayout); - mainLayout_->addLayout(fontSizeOptionLayout); - mainLayout_->addLayout(fontFamilyOptionLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(themeOptionLayout_); - mainLayout_->addWidget(new HorizontalLine(this)); - - mainLayout_->addSpacing(50); +#if !defined(Q_OS_MAC) + boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_); +#else + emojiFontSelectionCombo_->hide(); +#endif - mainLayout_->addWidget(encryptionLabel_, 1, Qt::AlignLeft | Qt::AlignBottom); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(encryptionLayout_); + boxWrap(tr("Theme"), themeCombo_); + formLayout_->addRow(encryptionLabel_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Device ID"), deviceIdValue_); + boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); + formLayout_->addRow(new HorizontalLine{this}); + formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); - auto scrollArea_ = new QScrollArea(this); + auto scrollArea_ = new QScrollArea{this}; scrollArea_->setFrameShape(QFrame::NoFrame); scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); scrollArea_->setWidgetResizable(true); scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); - auto scrollAreaContents_ = new QWidget(this); + QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); + + auto spacingAroundForm = new QHBoxLayout; + spacingAroundForm->addStretch(1); + spacingAroundForm->addLayout(formLayout_, 0); + spacingAroundForm->addStretch(1); + + auto scrollAreaContents_ = new QWidget{this}; scrollAreaContents_->setObjectName("UserSettingScrollWidget"); - scrollAreaContents_->setLayout(mainLayout_); + scrollAreaContents_->setLayout(spacingAroundForm); scrollArea_->setWidget(scrollAreaContents_); topLayout_->addLayout(topBarLayout_); @@ -392,6 +361,9 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge connect(fontSelectionCombo_, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); + connect(emojiFontSelectionCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), + [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTray(!isDisabled); if (isDisabled) { @@ -410,6 +382,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge settings_->setGroupView(!isDisabled); }); + connect(avatarCircles_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setAvatarCircles(!isDisabled); + }); + + connect(markdownEnabled_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setMarkdownEnabled(!isDisabled); + }); + connect(typingNotifications_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTypingNotifications(!isDisabled); }); @@ -422,6 +402,12 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge settings_->setDesktopNotifications(!isDisabled); }); + connect( + sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); + + connect( + sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); + connect(backBtn_, &QPushButton::clicked, this, [this]() { settings_->save(); emit moveBack(); @@ -440,8 +426,10 @@ UserSettingsPage::showEvent(QShowEvent *) trayToggle_->setState(!settings_->isTrayEnabled()); startInTrayToggle_->setState(!settings_->isStartInTrayEnabled()); groupViewToggle_->setState(!settings_->isGroupViewEnabled()); + avatarCircles_->setState(!settings_->isAvatarCirclesEnabled()); typingNotifications_->setState(!settings_->isTypingNotificationsEnabled()); readReceipts_->setState(!settings_->isReadReceiptsEnabled()); + markdownEnabled_->setState(!settings_->isMarkdownEnabled()); desktopNotifications_->setState(!settings_->hasDesktopNotifications()); deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); @@ -450,16 +438,6 @@ UserSettingsPage::showEvent(QShowEvent *) } void -UserSettingsPage::resizeEvent(QResizeEvent *event) -{ - sideMargin_ = width() * 0.2; - mainLayout_->setContentsMargins( - sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); - - QWidget::resizeEvent(event); -} - -void UserSettingsPage::paintEvent(QPaintEvent *) { QStyleOption opt; @@ -471,7 +449,9 @@ UserSettingsPage::paintEvent(QPaintEvent *) void UserSettingsPage::importSessionKeys() { - auto fileName = QFileDialog::getOpenFileName(this, tr("Open Sessions File"), "", ""); + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = + QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, ""); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { @@ -498,9 +478,9 @@ UserSettingsPage::importSessionKeys() } try { - auto sessions = mtx::crypto::decrypt_exported_sessions( - mtx::crypto::base642bin(payload), password.toStdString()); - cache::client()->importSessionKeys(std::move(sessions)); + auto sessions = + mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); + cache::importSessionKeys(std::move(sessions)); } catch (const mtx::crypto::sodium_exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const lmdb::error &e) { @@ -530,11 +510,12 @@ UserSettingsPage::exportSessionKeys() } // Open file dialog to save the file. - auto fileName = + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", ""); QFile file(fileName); - if (!file.open(QIODevice::WriteOnly)) { + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("Error"), file.errorString()); return; } @@ -542,11 +523,16 @@ UserSettingsPage::exportSessionKeys() // Export sessions & save to file. try { auto encrypted_blob = mtx::crypto::encrypt_exported_sessions( - cache::client()->exportSessionKeys(), password.toStdString()); + cache::exportSessionKeys(), password.toStdString()); - auto b64 = mtx::crypto::bin2base64(encrypted_blob); + QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob)); - file.write(b64.data(), b64.size()); + QString prefix("-----BEGIN MEGOLM SESSION DATA-----"); + QString suffix("-----END MEGOLM SESSION DATA-----"); + QString newline("\n"); + QTextStream out(&file); + out << prefix << newline << b64 << newline << suffix; + file.close(); } catch (const mtx::crypto::sodium_exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const lmdb::error &e) { diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 900f57e4..a1b7b084 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -19,9 +19,11 @@ #include <QComboBox> #include <QFontDatabase> +#include <QFormLayout> #include <QFrame> #include <QLabel> #include <QLayout> +#include <QProcessEnvironment> #include <QSharedPointer> #include <QWidget> @@ -56,6 +58,7 @@ public: void setFontSize(double size); void setFontFamily(QString family); + void setEmojiFontFamily(QString family); void setGroupView(bool state) { @@ -66,6 +69,12 @@ public: save(); } + void setMarkdownEnabled(bool state) + { + isMarkdownEnabled_ = state; + save(); + } + void setReadReceipts(bool state) { isReadReceiptsEnabled_ = state; @@ -84,29 +93,46 @@ public: save(); } - QString theme() const { return !theme_.isEmpty() ? theme_ : "light"; } + void setAvatarCircles(bool state) + { + avatarCircles_ = state; + save(); + } + + QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } bool isTrayEnabled() const { return isTrayEnabled_; } bool isStartInTrayEnabled() const { return isStartInTrayEnabled_; } bool isGroupViewEnabled() const { return isGroupViewEnabled_; } + bool isAvatarCirclesEnabled() const { return avatarCircles_; } + bool isMarkdownEnabled() const { return isMarkdownEnabled_; } bool isTypingNotificationsEnabled() const { return isTypingNotificationsEnabled_; } bool isReadReceiptsEnabled() const { return isReadReceiptsEnabled_; } bool hasDesktopNotifications() const { return hasDesktopNotifications_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } + QString emojiFont() const { return emojiFont_; } signals: void groupViewStateChanged(bool state); private: + // Default to system theme if QT_QPA_PLATFORMTHEME var is set. + QString defaultTheme_ = + QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() + ? "light" + : "system"; QString theme_; bool isTrayEnabled_; bool isStartInTrayEnabled_; bool isGroupViewEnabled_; + bool isMarkdownEnabled_; bool isTypingNotificationsEnabled_; bool isReadReceiptsEnabled_; bool hasDesktopNotifications_; + bool avatarCircles_; double baseFontSize_; QString font_; + QString emojiFont_; }; class HorizontalLine : public QFrame @@ -122,11 +148,10 @@ class UserSettingsPage : public QWidget Q_OBJECT public: - UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = 0); + UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = nullptr); protected: void showEvent(QShowEvent *event) override; - void resizeEvent(QResizeEvent *event) override; void paintEvent(QPaintEvent *event) override; signals: @@ -141,8 +166,8 @@ private slots: private: // Layouts QVBoxLayout *topLayout_; - QVBoxLayout *mainLayout_; QHBoxLayout *topBarLayout_; + QFormLayout *formLayout_; // Shared settings object. QSharedPointer<UserSettings> settings_; @@ -152,7 +177,9 @@ private: Toggle *groupViewToggle_; Toggle *typingNotifications_; Toggle *readReceipts_; + Toggle *markdownEnabled_; Toggle *desktopNotifications_; + Toggle *avatarCircles_; QLabel *deviceFingerprintValue_; QLabel *deviceIdValue_; @@ -160,6 +187,7 @@ private: QComboBox *scaleFactorCombo_; QComboBox *fontSizeCombo_; QComboBox *fontSelectionCombo_; + QComboBox *emojiFontSelectionCombo_; int sideMargin_ = 0; }; diff --git a/src/Utils.cpp b/src/Utils.cpp index f8fdfaf9..33b75894 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -3,25 +3,84 @@ #include <QApplication> #include <QComboBox> #include <QDesktopWidget> +#include <QGuiApplication> +#include <QProcessEnvironment> +#include <QScreen> #include <QSettings> #include <QTextDocument> #include <QXmlStreamReader> + #include <cmath> +#include <variant> -#include <boost/variant.hpp> #include <cmark.h> +#include "Cache.h" #include "Config.h" +#include "MatrixClient.h" using TimelineEvent = mtx::events::collections::TimelineEvents; QHash<QString, QString> authorColors_; +template<class T, class Event> +static DescInfo +createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) +{ + const auto msg = std::get<T>(event); + const auto sender = QString::fromStdString(msg.sender); + + const auto username = cache::displayName(room_id, sender); + const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); + + return DescInfo{ + QString::fromStdString(msg.event_id), + sender, + utils::messageDescription<T>( + username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser), + utils::descriptiveTime(ts), + ts}; +} + QString utils::localUser() { + return QString::fromStdString(http::client()->user_id().to_string()); +} + +QString +utils::replaceEmoji(const QString &body) +{ + QString fmtBody = ""; + + QVector<uint> utf32_string = body.toUcs4(); + QSettings settings; - return settings.value("auth/user_id").toString(); + QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); + + bool insideFontBlock = false; + for (auto &code : utf32_string) { + // TODO: Be more precise here. + if ((code >= 0x2600 && code <= 0x27bf) || (code >= 0x1f300 && code <= 0x1f3ff) || + (code >= 0x1f000 && code <= 0x1faff)) { + if (!insideFontBlock) { + fmtBody += QString("<font face=\"" + userFontFamily + "\">"); + insideFontBlock = true; + } + + } else { + if (insideFontBlock) { + fmtBody += "</font>"; + insideFontBlock = false; + } + } + fmtBody += QString::fromUcs4(&code, 1); + } + if (insideFontBlock) { + fmtBody += "</font>"; + } + + return fmtBody; } void @@ -37,7 +96,7 @@ utils::setScaleFactor(float factor) float utils::scaleFactor() { - QSettings settings("nheko", "nheko"); + QSettings settings; return settings.value("settings/scale_factor", -1).toFloat(); } @@ -74,13 +133,13 @@ utils::descriptiveTime(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return then.toString("HH:mm"); + return then.time().toString(Qt::DefaultLocaleShortDate); else if (days < 2) - return QString("Yesterday"); - else if (days < 365) - return then.toString("dd/MM"); + return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); + else if (days < 7) + return then.toString("dddd"); - return then.toString("dd/MM/yy"); + return then.date().toString(Qt::DefaultLocaleShortDate); } DescInfo @@ -97,39 +156,33 @@ utils::getMessageDescription(const TimelineEvent &event, using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; - if (boost::get<Audio>(&event) != nullptr) { + if (std::holds_alternative<Audio>(event)) { return createDescriptionInfo<Audio>(event, localUser, room_id); - } else if (boost::get<Emote>(&event) != nullptr) { + } else if (std::holds_alternative<Emote>(event)) { return createDescriptionInfo<Emote>(event, localUser, room_id); - } else if (boost::get<File>(&event) != nullptr) { + } else if (std::holds_alternative<File>(event)) { return createDescriptionInfo<File>(event, localUser, room_id); - } else if (boost::get<Image>(&event) != nullptr) { + } else if (std::holds_alternative<Image>(event)) { return createDescriptionInfo<Image>(event, localUser, room_id); - } else if (boost::get<Notice>(&event) != nullptr) { + } else if (std::holds_alternative<Notice>(event)) { return createDescriptionInfo<Notice>(event, localUser, room_id); - } else if (boost::get<Text>(&event) != nullptr) { + } else if (std::holds_alternative<Text>(event)) { return createDescriptionInfo<Text>(event, localUser, room_id); - } else if (boost::get<Video>(&event) != nullptr) { + } else if (std::holds_alternative<Video>(event)) { return createDescriptionInfo<Video>(event, localUser, room_id); - } else if (boost::get<mtx::events::Sticker>(&event) != nullptr) { + } else if (std::holds_alternative<mtx::events::Sticker>(event)) { return createDescriptionInfo<mtx::events::Sticker>(event, localUser, room_id); - } else if (boost::get<Encrypted>(&event) != nullptr) { - const auto msg = boost::get<Encrypted>(event); - const auto sender = QString::fromStdString(msg.sender); + } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) { + const auto sender = QString::fromStdString(msg->sender); - const auto username = Cache::displayName(room_id, sender); - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); + const auto username = cache::displayName(room_id, sender); + const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts); DescInfo info; - if (sender == localUser) - info.username = "You"; - else - info.username = username; - info.userid = sender; info.body = QString(" %1").arg(messageDescription<Encrypted>()); info.timestamp = utils::descriptiveTime(ts); - info.event_id = QString::fromStdString(msg.event_id); + info.event_id = QString::fromStdString(msg->event_id); info.datetime = ts; return info; @@ -197,30 +250,25 @@ utils::levenshtein_distance(const std::string &s1, const std::string &s2) } QString -utils::event_body(const mtx::events::collections::TimelineEvents &event) +utils::event_body(const mtx::events::collections::TimelineEvents &e) { using namespace mtx::events; - using namespace mtx::events::msg; - - if (boost::get<RoomEvent<Audio>>(&event) != nullptr) { - return message_body<RoomEvent<Audio>>(event); - } else if (boost::get<RoomEvent<Emote>>(&event) != nullptr) { - return message_body<RoomEvent<Emote>>(event); - } else if (boost::get<RoomEvent<File>>(&event) != nullptr) { - return message_body<RoomEvent<File>>(event); - } else if (boost::get<RoomEvent<Image>>(&event) != nullptr) { - return message_body<RoomEvent<Image>>(event); - } else if (boost::get<RoomEvent<Notice>>(&event) != nullptr) { - return message_body<RoomEvent<Notice>>(event); - } else if (boost::get<Sticker>(&event) != nullptr) { - return message_body<Sticker>(event); - } else if (boost::get<RoomEvent<Text>>(&event) != nullptr) { - return message_body<RoomEvent<Text>>(event); - } else if (boost::get<RoomEvent<Video>>(&event) != nullptr) { - return message_body<RoomEvent<Video>>(event); - } - - return QString(); + if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + + return ""; } QPixmap @@ -229,8 +277,10 @@ utils::scaleImageToPixmap(const QImage &img, int size) if (img.isNull()) return QPixmap(); + // Deprecated in 5.13: const double sz = + // std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); const double sz = - std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); + std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size); return QPixmap::fromImage( img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } @@ -290,18 +340,72 @@ QString utils::linkifyMessage(const QString &body) { // Convert to valid XML. - auto doc = QString("<html>%1</html>").arg(body); + auto doc = body; doc.replace(conf::strings::url_regex, conf::strings::url_html); return doc; } QString +utils::escapeBlacklistedHtml(const QString &rawStr) +{ + static const std::array allowedTags = { + "font", "/font", "del", "/del", "h1", "/h1", "h2", "/h2", + "h3", "/h3", "h4", "/h4", "h5", "/h5", "h6", "/h6", + "blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul", + "ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li", + "b", "/b", "i", "/i", "u", "/u", "strong", "/strong", + "em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr", + "br", "br/", "div", "/div", "table", "/table", "thead", "/thead", + "tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td", + "caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img"}; + QByteArray data = rawStr.toUtf8(); + QByteArray buffer; + const size_t length = data.size(); + buffer.reserve(length); + bool escapingTag = false; + for (size_t pos = 0; pos != length; ++pos) { + switch (data.at(pos)) { + case '<': { + bool oneTagMatched = false; + size_t endPos = std::min(static_cast<size_t>(data.indexOf('>', pos)), + static_cast<size_t>(data.indexOf(' ', pos))); + + auto mid = data.mid(pos + 1, endPos - pos - 1); + for (const auto &tag : allowedTags) { + // TODO: Check src and href attribute + if (mid.toLower() == tag) { + oneTagMatched = true; + } + } + if (oneTagMatched) + buffer.append('<'); + else { + escapingTag = true; + buffer.append("<"); + } + break; + } + case '>': + if (escapingTag) { + buffer.append(">"); + escapingTag = false; + } else + buffer.append('>'); + break; + default: + buffer.append(data.at(pos)); + break; + } + } + return QString::fromUtf8(buffer); +} + +QString utils::markdownToHtml(const QString &text) { - const auto str = text.toUtf8(); - const char *tmp_buf = - cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_DEFAULT); + const auto str = text.toUtf8(); + const char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_UNSAFE); // Copy the null terminated output buffer. std::string html(tmp_buf); @@ -309,29 +413,99 @@ utils::markdownToHtml(const QString &text) // The buffer is no longer needed. free((char *)tmp_buf); - auto result = QString::fromStdString(html).trimmed(); + auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed(); + + if (result.count("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</p>")) { + result = result.mid(3, result.size() - 3 - 4); + } return result; } QString +utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) +{ + auto getFormattedBody = [related]() -> QString { + using MsgType = mtx::events::MessageType; + + switch (related.type) { + case MsgType::File: { + return "sent a file."; + } + case MsgType::Image: { + return "sent an image."; + } + case MsgType::Audio: { + return "sent an audio file."; + } + case MsgType::Video: { + return "sent a video"; + } + default: { + return related.quoted_formatted_body; + } + } + }; + return QString("<mx-reply><blockquote><a " + "href=\"https://matrix.to/#/%1/%2\">In reply " + "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br" + "/>%5</blockquote></mx-reply>") + .arg(related.room, + QString::fromStdString(related.related_event), + related.quoted_user, + related.quoted_user, + getFormattedBody()) + + html; +} + +QString +utils::getQuoteBody(const RelatedInfo &related) +{ + using MsgType = mtx::events::MessageType; + + switch (related.type) { + case MsgType::File: { + return "sent a file."; + } + case MsgType::Image: { + return "sent an image."; + } + case MsgType::Audio: { + return "sent an audio file."; + } + case MsgType::Video: { + return "sent a video"; + } + default: { + return related.quoted_body; + } + } +} + +QString utils::linkColor() { QSettings settings; - const auto theme = settings.value("user/theme", "light").toString(); - - if (theme == "light") + // Default to system theme if QT_QPA_PLATFORMTHEME var is set. + QString defaultTheme = + QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() + ? "light" + : "system"; + const auto theme = settings.value("user/theme", defaultTheme).toString(); + + if (theme == "light") { return "#0077b5"; - else if (theme == "dark") + } else if (theme == "dark") { return "#38A3D8"; - - return QPalette().color(QPalette::Link).name(); + } else { + return QPalette().color(QPalette::Link).name(); + } } -int +uint32_t utils::hashQString(const QString &input) { - auto hash = 0; + uint32_t hash = 0; for (int i = 0; i < input.length(); i++) { hash = input.at(i).digitValue() + ((hash << 5) - hash); @@ -349,7 +523,7 @@ utils::generateContrastingHexColor(const QString &input, const QString &backgrou // 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); + auto userHue = static_cast<int>(qAbs(hash % 360)); // start with moderate saturation and lightness values. auto sat = 220; auto lightness = 125; @@ -457,11 +631,13 @@ utils::centerWidget(QWidget *widget, QWidget *parent) }; if (parent) { - widget->move(findCenter(parent->geometry())); + widget->move(parent->window()->frameGeometry().topLeft() + + parent->window()->rect().center() - widget->rect().center()); return; } - widget->move(findCenter(QApplication::desktop()->screenGeometry())); + // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry())); + widget->move(findCenter(QGuiApplication::primaryScreen()->geometry())); } void @@ -474,17 +650,3 @@ utils::restoreCombobox(QComboBox *combo, const QString &value) } } } - -utils::SideBarSizes -utils::calculateSidebarSizes(const QFont &f) -{ - const auto height = static_cast<double>(QFontMetrics{f}.lineSpacing()); - - SideBarSizes sz; - sz.small = std::ceil(3.5 * height + height / 4.0); - sz.normal = std::ceil(16 * height); - sz.groups = std::ceil(3 * height); - sz.collapsePoint = 2 * sz.normal; - - return sz; -} diff --git a/src/Utils.h b/src/Utils.h index 8672e7d4..a3854dd8 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -1,14 +1,8 @@ #pragma once -#include <boost/variant.hpp> - -#include "Cache.h" -#include "RoomInfoListItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" +#include <variant> +#include <QCoreApplication> #include <QDateTime> #include <QPixmap> #include <mtx/events/collections.hpp> @@ -16,13 +10,36 @@ #include <qmath.h> +struct DescInfo; + +namespace cache { +// Forward declarations to prevent dependency on Cache.h, since this header is included often! +QString +displayName(const QString &room_id, const QString &user_id); +} + class QComboBox; +// Contains information about related events for +// outgoing messages +struct RelatedInfo +{ + using MsgType = mtx::events::MessageType; + MsgType type; + QString room; + QString quoted_body, quoted_formatted_body; + std::string related_event; + QString quoted_user; +}; + namespace utils { using TimelineEvent = mtx::events::collections::TimelineEvents; QString +replaceEmoji(const QString &body); + +QString localUser(); float @@ -64,7 +81,9 @@ event_body(const mtx::events::collections::TimelineEvents &event); //! Match widgets/events with a description message. template<class T> QString -messageDescription(const QString &username = "", const QString &body = "") +messageDescription(const QString &username = "", + const QString &body = "", + const bool isLocal = false) { using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>; using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; @@ -76,51 +95,75 @@ messageDescription(const QString &username = "", const QString &body = "") using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; - if (std::is_same<T, AudioItem>::value || std::is_same<T, Audio>::value) - return QString("sent an audio clip"); - else if (std::is_same<T, ImageItem>::value || std::is_same<T, Image>::value) - return QString("sent an image"); - else if (std::is_same<T, FileItem>::value || std::is_same<T, File>::value) - return QString("sent a file"); - else if (std::is_same<T, VideoItem>::value || std::is_same<T, Video>::value) - return QString("sent a video clip"); - else if (std::is_same<T, StickerItem>::value || std::is_same<T, Sticker>::value) - return QString("sent a sticker"); - else if (std::is_same<T, Notice>::value) - return QString("sent a notification"); - else if (std::is_same<T, Text>::value) - return QString(": %1").arg(body); - else if (std::is_same<T, Emote>::value) + if (std::is_same<T, Audio>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an audio clip"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an audio clip") + .arg(username); + } else if (std::is_same<T, Image>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an image"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an image") + .arg(username); + } else if (std::is_same<T, File>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a file"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a file") + .arg(username); + } else if (std::is_same<T, Video>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a video"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a video") + .arg(username); + } else if (std::is_same<T, Sticker>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a sticker"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a sticker") + .arg(username); + } else if (std::is_same<T, Notice>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a notification"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a notification") + .arg(username); + } else if (std::is_same<T, Text>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You: %1") + .arg(body); + else + return QCoreApplication::translate("message-description sent:", "%1: %2") + .arg(username) + .arg(body); + } else if (std::is_same<T, Emote>::value) { return QString("* %1 %2").arg(username).arg(body); - else if (std::is_same<T, Encrypted>::value) - return QString("sent an encrypted message"); -} - -template<class T, class Event> -DescInfo -createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) -{ - using Text = mtx::events::RoomEvent<mtx::events::msg::Text>; - using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; - - const auto msg = boost::get<T>(event); - const auto sender = QString::fromStdString(msg.sender); - - const auto username = Cache::displayName(room_id, sender); - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - - bool isText = std::is_same<T, Text>::value; - bool isEmote = std::is_same<T, Emote>::value; - - return DescInfo{ - QString::fromStdString(msg.event_id), - isEmote ? "" : (sender == localUser ? "You" : username), - sender, - (isText || isEmote) - ? messageDescription<T>(username, QString::fromStdString(msg.content.body).trimmed()) - : QString(" %1").arg(messageDescription<T>()), - utils::descriptiveTime(ts), - ts}; + } else if (std::is_same<T, Encrypted>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an encrypted message"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an encrypted message") + .arg(username); + } else { + return QCoreApplication::translate("utils", "Unknown Message Type"); + } } //! Scale down an image to fit to the given width & height limitations. @@ -143,25 +186,25 @@ erase_if(ContainerT &items, const PredicateT &predicate) inline uint64_t event_timestamp(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return msg.origin_server_ts; }, event); + return std::visit([](auto msg) { return msg.origin_server_ts; }, event); } inline nlohmann::json serialize_event(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return json(msg); }, event); + return std::visit([](auto msg) { return json(msg); }, event); } inline mtx::events::EventType event_type(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return msg.type; }, event); + return std::visit([](auto msg) { return msg.type; }, event); } inline std::string event_id(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return msg.event_id; }, event); + return std::visit([](auto msg) { return msg.event_id; }, event); } inline QString @@ -173,15 +216,14 @@ eventId(const mtx::events::collections::TimelineEvents &event) inline QString event_sender(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return QString::fromStdString(msg.sender); }, - event); + return std::visit([](auto msg) { return QString::fromStdString(msg.sender); }, event); } template<class T> QString message_body(const mtx::events::collections::TimelineEvents &event) { - return QString::fromStdString(boost::get<T>(event).content.body); + return QString::fromStdString(std::get<T>(event).content.body); } //! Calculate the Levenshtein distance between two strings with character skipping. @@ -211,7 +253,7 @@ getMessageBody(const RoomMessageT &event) if (event.content.format.empty()) return QString::fromStdString(event.content.body).toHtmlEscaped(); - if (event.content.format != common::FORMAT_MSG_TYPE) + if (event.content.format != mtx::common::FORMAT_MSG_TYPE) return QString::fromStdString(event.content.body).toHtmlEscaped(); return QString::fromStdString(event.content.formatted_body); @@ -225,12 +267,24 @@ linkifyMessage(const QString &body); QString markdownToHtml(const QString &text); +//! Escape every html tag, that was not whitelisted +QString +escapeBlacklistedHtml(const QString &data); + +//! Generate a Rich Reply quote message +QString +getFormattedQuoteBody(const RelatedInfo &related, const QString &html); + +//! Get the body for the quote, depending on the event type. +QString +getQuoteBody(const RelatedInfo &related); + //! Retrieve the color of the links based on the current theme. QString linkColor(); //! Returns the hash code of the input QString -int +uint32_t hashQString(const QString &input); //! Generate a color (matching #RRGGBB) that has an acceptable contrast to background that is based @@ -253,14 +307,4 @@ centerWidget(QWidget *widget, QWidget *parent); void restoreCombobox(QComboBox *combo, const QString &value); -struct SideBarSizes -{ - int small; - int normal; - int groups; - int collapsePoint; -}; - -SideBarSizes -calculateSidebarSizes(const QFont &f); } diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp index 8c3f6487..e4b0e1c6 100644 --- a/src/WelcomePage.cpp +++ b/src/WelcomePage.cpp @@ -17,6 +17,7 @@ #include <QLabel> #include <QLayout> +#include <QPainter> #include <QStyleOption> #include "Config.h" diff --git a/src/WelcomePage.h b/src/WelcomePage.h index 480dc702..ae660215 100644 --- a/src/WelcomePage.h +++ b/src/WelcomePage.h @@ -7,7 +7,7 @@ class WelcomePage : public QWidget Q_OBJECT public: - explicit WelcomePage(QWidget *parent = 0); + explicit WelcomePage(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *) override; diff --git a/src/dialogs/CreateRoom.h b/src/dialogs/CreateRoom.h index 22ac6a43..a482a636 100644 --- a/src/dialogs/CreateRoom.h +++ b/src/dialogs/CreateRoom.h @@ -2,7 +2,7 @@ #include <QFrame> -#include <mtx.hpp> +#include <mtx/requests.hpp> class QPushButton; class TextField; diff --git a/src/dialogs/FallbackAuth.cpp b/src/dialogs/FallbackAuth.cpp new file mode 100644 index 00000000..a0633c1e --- /dev/null +++ b/src/dialogs/FallbackAuth.cpp @@ -0,0 +1,69 @@ +#include <QDesktopServices> +#include <QLabel> +#include <QPushButton> +#include <QUrl> +#include <QVBoxLayout> + +#include "dialogs/FallbackAuth.h" + +#include "Config.h" +#include "MatrixClient.h" + +using namespace dialogs; + +FallbackAuth::FallbackAuth(const QString &authType, const QString &session, QWidget *parent) + : QWidget(parent) +{ + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); + + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(8); + buttonLayout->setMargin(0); + + openBtn_ = new QPushButton(tr("Open Fallback in Browser"), this); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + confirmBtn_ = new QPushButton(tr("Confirm"), this); + confirmBtn_->setDefault(true); + + buttonLayout->addStretch(1); + buttonLayout->addWidget(openBtn_); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); + + QFont font; + font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); + + auto label = new QLabel( + tr("Open the fallback, follow the steps and confirm after completing them."), this); + label->setFont(font); + + layout->addWidget(label); + layout->addLayout(buttonLayout); + + connect(openBtn_, &QPushButton::clicked, [session, authType]() { + const auto url = QString("https://%1:%2/_matrix/client/r0/auth/%4/" + "fallback/web?session=%3") + .arg(QString::fromStdString(http::client()->server())) + .arg(http::client()->port()) + .arg(session) + .arg(authType); + + QDesktopServices::openUrl(url); + }); + + connect(confirmBtn_, &QPushButton::clicked, this, [this]() { + emit confirmation(); + emit close(); + }); + connect(cancelBtn_, &QPushButton::clicked, this, [this]() { + emit cancel(); + emit close(); + }); +} diff --git a/src/dialogs/FallbackAuth.h b/src/dialogs/FallbackAuth.h new file mode 100644 index 00000000..245fa03e --- /dev/null +++ b/src/dialogs/FallbackAuth.h @@ -0,0 +1,26 @@ +#pragma once + +#include <QWidget> + +class QPushButton; +class QLabel; + +namespace dialogs { + +class FallbackAuth : public QWidget +{ + Q_OBJECT + +public: + FallbackAuth(const QString &authType, const QString &session, QWidget *parent = nullptr); + +signals: + void confirmation(); + void cancel(); + +private: + QPushButton *openBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; +}; +} // dialogs diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp index dbf5bbe4..e075fb67 100644 --- a/src/dialogs/ImageOverlay.cpp +++ b/src/dialogs/ImageOverlay.cpp @@ -17,7 +17,9 @@ #include <QApplication> #include <QDesktopWidget> +#include <QGuiApplication> #include <QPainter> +#include <QScreen> #include "dialogs/ImageOverlay.h" @@ -30,7 +32,7 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) , originalImage_{image} { setMouseTracking(true); - setParent(0); + setParent(nullptr); setWindowFlags(windowFlags() | Qt::FramelessWindowHint); @@ -39,11 +41,6 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose, true); setWindowState(Qt::WindowFullScreen); - screen_ = QApplication::desktop()->availableGeometry(); - - move(QApplication::desktop()->mapToGlobal(screen_.topLeft())); - resize(screen_.size()); - connect(this, SIGNAL(closing()), this, SLOT(close())); raise(); @@ -58,15 +55,15 @@ ImageOverlay::paintEvent(QPaintEvent *event) painter.setRenderHint(QPainter::Antialiasing); // Full screen overlay. - painter.fillRect(QRect(0, 0, screen_.width(), screen_.height()), QColor(55, 55, 55, 170)); + painter.fillRect(QRect(0, 0, width(), height()), QColor(55, 55, 55, 170)); // Left and Right margins - int outer_margin = screen_.width() * 0.12; + int outer_margin = width() * 0.12; int buttonSize = 36; int margin = outer_margin * 0.1; - int max_width = screen_.width() - 2 * outer_margin; - int max_height = screen_.height(); + int max_width = width() - 2 * outer_margin; + int max_height = height(); image_ = utils::scaleDown(max_width, max_height, originalImage_); @@ -74,10 +71,9 @@ ImageOverlay::paintEvent(QPaintEvent *event) int diff_y = max_height - image_.height(); content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height()); - close_button_ = - QRect(screen_.width() - margin - buttonSize, margin, buttonSize, buttonSize); + close_button_ = QRect(width() - margin - buttonSize, margin, buttonSize, buttonSize); save_button_ = - QRect(screen_.width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); + QRect(width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); // Draw main content_. painter.drawPixmap(content_, image_); diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h index 26257fc1..bf566ce4 100644 --- a/src/dialogs/ImageOverlay.h +++ b/src/dialogs/ImageOverlay.h @@ -44,6 +44,5 @@ private: QRect content_; QRect close_button_; QRect save_button_; - QRect screen_; }; } // dialogs diff --git a/src/dialogs/InviteUsers.cpp b/src/dialogs/InviteUsers.cpp index bacfe498..691035ce 100644 --- a/src/dialogs/InviteUsers.cpp +++ b/src/dialogs/InviteUsers.cpp @@ -13,7 +13,7 @@ #include "InviteeItem.h" #include "ui/TextField.h" -#include "mtx.hpp" +#include <mtx/identifiers.hpp> using namespace dialogs; diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index f4167143..54e7bf96 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -1,4 +1,5 @@ #include <QAbstractSlider> +#include <QLabel> #include <QListWidgetItem> #include <QPainter> #include <QPushButton> @@ -9,10 +10,10 @@ #include "dialogs/MemberList.h" -#include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" #include "Config.h" +#include "Logging.h" #include "Utils.h" #include "ui/Avatar.h" @@ -28,17 +29,10 @@ MemberItem::MemberItem(const RoomMember &member, QWidget *parent) textLayout_->setMargin(0); textLayout_->setSpacing(0); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(member.display_name)); - if (!member.avatar.isNull()) - avatar_->setImage(member.avatar); - else - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - member.user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); @@ -97,7 +91,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) topLabel_->setAlignment(Qt::AlignCenter); topLabel_->setFont(font); - auto okBtn = new QPushButton("OK", this); + auto okBtn = new QPushButton(tr("OK"), this); auto buttonLayout = new QHBoxLayout(); buttonLayout->setSpacing(15); @@ -117,16 +111,16 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) const size_t numMembers = list_->count() - 1; if (numMembers > 0) - addUsers(cache::client()->getMembers(room_id_.toStdString(), numMembers)); + addUsers(cache::getMembers(room_id_.toStdString(), numMembers)); }); try { - addUsers(cache::client()->getMembers(room_id_.toStdString())); + addUsers(cache::getMembers(room_id_.toStdString())); } catch (const lmdb::error &e) { - qCritical() << e.what(); + nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); } - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &MemberList::close); connect(okBtn, &QPushButton::clicked, this, &MemberList::close); } diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp index c404799e..42558d67 100644 --- a/src/dialogs/PreviewUploadOverlay.cpp +++ b/src/dialogs/PreviewUploadOverlay.cpp @@ -15,7 +15,6 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QApplication> #include <QBuffer> #include <QFile> #include <QFileInfo> @@ -135,6 +134,28 @@ PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64 } void +PreviewUploadOverlay::setPreview(const QImage &src, const QString &mime) +{ + auto const &split = mime.split('/'); + auto const &type = split[1]; + + QBuffer buffer(&data_); + buffer.open(QIODevice::WriteOnly); + if (src.save(&buffer, type.toStdString().c_str())) + titleLabel_.setText(QString{tr(DEFAULT)}.arg("image")); + else + titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); + + mediaType_ = split[0]; + filePath_ = "clipboard." + type; + image_.convertFromImage(src); + isImage_ = true; + + titleLabel_.setText(QString{tr(DEFAULT)}.arg("image")); + init(); +} + +void PreviewUploadOverlay::setPreview(const QByteArray data, const QString &mime) { auto const &split = mime.split('/'); diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h index 8099d9c2..11cd49bc 100644 --- a/src/dialogs/PreviewUploadOverlay.h +++ b/src/dialogs/PreviewUploadOverlay.h @@ -17,6 +17,7 @@ #pragma once +#include <QImage> #include <QLabel> #include <QLineEdit> #include <QPixmap> @@ -33,6 +34,7 @@ class PreviewUploadOverlay : public QWidget public: PreviewUploadOverlay(QWidget *parent = nullptr); + void setPreview(const QImage &src, const QString &mime); void setPreview(const QByteArray data, const QString &mime); void setPreview(const QString &path); diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp index 7849aa4f..21dc8c77 100644 --- a/src/dialogs/ReCaptcha.cpp +++ b/src/dialogs/ReCaptcha.cpp @@ -60,5 +60,8 @@ ReCaptcha::ReCaptcha(const QString &session, QWidget *parent) emit confirmation(); emit close(); }); - connect(cancelBtn_, &QPushButton::clicked, this, &dialogs::ReCaptcha::close); + connect(cancelBtn_, &QPushButton::clicked, this, [this]() { + emit cancel(); + emit close(); + }); } diff --git a/src/dialogs/ReCaptcha.h b/src/dialogs/ReCaptcha.h index f8407640..88ff3722 100644 --- a/src/dialogs/ReCaptcha.h +++ b/src/dialogs/ReCaptcha.h @@ -15,6 +15,7 @@ public: signals: void confirmation(); + void cancel(); private: QPushButton *openCaptchaBtn_; diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp index dc4145db..0edd1ebf 100644 --- a/src/dialogs/ReadReceipts.cpp +++ b/src/dialogs/ReadReceipts.cpp @@ -35,10 +35,9 @@ ReceiptItem::ReceiptItem(QWidget *parent, QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - auto displayName = Cache::displayName(room_id, user_id); + auto displayName = cache::displayName(room_id, user_id); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(displayName)); // If it's a matrix id we use the second letter. @@ -56,10 +55,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, topLayout_->addWidget(avatar_); topLayout_->addLayout(textLayout_, 1); - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); } void @@ -78,13 +74,15 @@ ReceiptItem::dateFormat(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return QString("Today %1").arg(then.toString("HH:mm")); + return tr("Today %1").arg(then.time().toString(Qt::DefaultLocaleShortDate)); else if (days < 2) - return QString("Yesterday %1").arg(then.toString("HH:mm")); - else if (days < 365) - return then.toString("dd/MM HH:mm"); + return tr("Yesterday %1").arg(then.time().toString(Qt::DefaultLocaleShortDate)); + else if (days < 7) + return QString("%1 %2") + .arg(then.toString("dddd")) + .arg(then.time().toString(Qt::DefaultLocaleShortDate)); - return then.toString("dd/MM/yy"); + return then.toString(Qt::DefaultLocaleShortDate); } ReadReceipts::ReadReceipts(QWidget *parent) @@ -131,7 +129,7 @@ ReadReceipts::ReadReceipts(QWidget *parent) layout->addWidget(userList_); layout->addLayout(buttonLayout); - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close); connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close); } diff --git a/src/dialogs/ReadReceipts.h b/src/dialogs/ReadReceipts.h index 8e1b6b75..e298af0a 100644 --- a/src/dialogs/ReadReceipts.h +++ b/src/dialogs/ReadReceipts.h @@ -22,7 +22,7 @@ public: const QString &room_id); protected: - void paintEvent(QPaintEvent *); + void paintEvent(QPaintEvent *) override; private: QString dateFormat(const QDateTime &then) const; diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index f9b7e913..cc10ac91 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -11,11 +11,13 @@ #include <QPushButton> #include <QShortcut> #include <QShowEvent> +#include <QStandardPaths> #include <QStyleOption> #include <QVBoxLayout> #include "dialogs/RoomSettings.h" +#include "Cache.h" #include "ChatPage.h" #include "Config.h" #include "Logging.h" @@ -199,12 +201,112 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) Qt::AlignBottom | Qt::AlignLeft); roomIdLayout->addWidget(roomIdLabel, 0, Qt::AlignBottom | Qt::AlignRight); + auto roomVersionLabel = new QLabel(QString::fromStdString(info_.version), this); + roomVersionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + roomVersionLabel->setFont(monospaceFont); + + auto roomVersionLayout = new QHBoxLayout; + roomVersionLayout->setMargin(0); + roomVersionLayout->addWidget(new QLabel(tr("Room Version"), this), + Qt::AlignBottom | Qt::AlignLeft); + roomVersionLayout->addWidget(roomVersionLabel, 0, Qt::AlignBottom | Qt::AlignRight); + auto notifLabel = new QLabel(tr("Notifications"), this); - auto notifCombo = new QComboBox(this); - notifCombo->setDisabled(true); - notifCombo->addItem(tr("Muted")); - notifCombo->addItem(tr("Mentions only")); - notifCombo->addItem(tr("All messages")); + notifCombo = new QComboBox(this); + notifCombo->addItem(tr( + "Muted")); //{"conditions":[{"kind":"event_match","key":"room_id","pattern":"!jxlRxnrZCsjpjDubDX:matrix.org"}],"actions":["dont_notify"]} + notifCombo->addItem(tr("Mentions only")); // {"actions":["dont_notify"]} + notifCombo->addItem(tr("All messages")); // delete rule + + connect(this, &RoomSettings::notifChanged, notifCombo, &QComboBox::setCurrentIndex); + http::client()->get_pushrules( + "global", + "override", + room_id_.toStdString(), + [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) { + if (err) { + if (err->status_code == boost::beast::http::status::not_found) + http::client()->get_pushrules( + "global", + "room", + room_id_.toStdString(), + [this](const mtx::pushrules::PushRule &rule, + mtx::http::RequestErr &err) { + if (err) { + emit notifChanged(2); // all messages + return; + } + + if (rule.enabled) + emit notifChanged(1); // mentions only + }); + return; + } + + if (rule.enabled) + emit notifChanged(0); // muted + else + emit notifChanged(2); // all messages + }); + + connect(notifCombo, QOverload<int>::of(&QComboBox::activated), [this](int index) { + std::string room_id = room_id_.toStdString(); + if (index == 0) { + // mute room + // delete old rule first, then add new rule + mtx::pushrules::PushRule rule; + rule.actions = {mtx::pushrules::actions::dont_notify{}}; + mtx::pushrules::PushCondition condition; + condition.kind = "event_match"; + condition.key = "room_id"; + condition.pattern = room_id; + rule.conditions = {condition}; + + http::client()->put_pushrules( + "global", + "override", + room_id, + rule, + [room_id](mtx::http::RequestErr &err) { + if (err) + nhlog::net()->error( + "failed to set pushrule for room {}: {} {}", + room_id, + static_cast<int>(err->status_code), + err->matrix_error.error); + http::client()->delete_pushrules( + "global", "room", room_id, [room_id](mtx::http::RequestErr &) { + }); + }); + } else if (index == 1) { + // mentions only + // delete old rule first, then add new rule + mtx::pushrules::PushRule rule; + rule.actions = {mtx::pushrules::actions::dont_notify{}}; + http::client()->put_pushrules( + "global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) { + if (err) + nhlog::net()->error( + "failed to set pushrule for room {}: {} {}", + room_id, + static_cast<int>(err->status_code), + err->matrix_error.error); + http::client()->delete_pushrules( + "global", + "override", + room_id, + [room_id](mtx::http::RequestErr &) {}); + }); + } else { + // all messages + http::client()->delete_pushrules( + "global", "override", room_id, [room_id](mtx::http::RequestErr &) { + http::client()->delete_pushrules( + "global", "room", room_id, [room_id](mtx::http::RequestErr &) { + }); + }); + } + }); auto notifOptionLayout_ = new QHBoxLayout; notifOptionLayout_->setMargin(0); @@ -238,10 +340,10 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) switch (index) { case 0: case 1: - event.join_rule = JoinRule::Public; + event.join_rule = state::JoinRule::Public; break; default: - event.join_rule = JoinRule::Invite; + event.join_rule = state::JoinRule::Invite; } return event; @@ -250,7 +352,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) updateAccessRules(room_id_.toStdString(), join_rule, guest_access); }); - if (info_.join_rule == JoinRule::Public) { + if (info_.join_rule == state::JoinRule::Public) { if (info_.guest_access) { accessCombo->setCurrentIndex(0); } else { @@ -332,7 +434,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) } // Hide encryption option for public rooms. - if (!usesEncryption_ && (info_.join_rule == JoinRule::Public)) { + if (!usesEncryption_ && (info_.join_rule == state::JoinRule::Public)) { encryptionToggle_->hide(); encryptionLabel->hide(); @@ -340,12 +442,10 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) keyRequestsToggle_->hide(); } - avatar_ = new Avatar(this); - avatar_->setSize(128); - if (avatarImg_.isNull()) - avatar_->setLetter(utils::firstChar(QString::fromStdString(info_.name))); - else - avatar_->setImage(avatarImg_); + avatar_ = new Avatar(this, 128); + avatar_->setLetter(utils::firstChar(QString::fromStdString(info_.name))); + if (!info_.avatar_url.empty()) + avatar_->setImage(QString::fromStdString(info_.avatar_url)); if (canChangeAvatar(room_id_.toStdString(), utils::localUser().toStdString())) { auto filter = new ClickableFilter(this); @@ -400,6 +500,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) layout->addLayout(keyRequestsLayout); layout->addWidget(infoLabel, Qt::AlignLeft); layout->addLayout(roomIdLayout); + layout->addLayout(roomVersionLayout); layout->addWidget(errorLabel_); layout->addLayout(buttonLayout); layout->addLayout(spinnerLayout); @@ -427,7 +528,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) resetErrorLabel(); }); - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &RoomSettings::close); connect(okBtn, &QPushButton::clicked, this, &RoomSettings::close); } @@ -474,10 +575,10 @@ void RoomSettings::retrieveRoomInfo() { try { - usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); - info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); - setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url))); - } catch (const lmdb::error &e) { + usesEncryption_ = cache::isRoomEncrypted(room_id_.toStdString()); + info_ = cache::singleRoomInfo(room_id_.toStdString()); + setAvatar(); + } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); } @@ -518,8 +619,7 @@ bool RoomSettings::canChangeJoinRules(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( - {EventType::RoomJoinRules}, room_id, user_id); + return cache::hasEnoughPowerLevel({EventType::RoomJoinRules}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); } @@ -531,7 +631,7 @@ bool RoomSettings::canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( + return cache::hasEnoughPowerLevel( {EventType::RoomName, EventType::RoomTopic}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); @@ -544,8 +644,7 @@ bool RoomSettings::canChangeAvatar(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( - {EventType::RoomAvatar}, room_id, user_id); + return cache::hasEnoughPowerLevel({EventType::RoomAvatar}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); } @@ -622,14 +721,12 @@ RoomSettings::displayErrorMessage(const QString &msg) } void -RoomSettings::setAvatar(const QImage &img) +RoomSettings::setAvatar() { stopLoadingSpinner(); - avatarImg_ = img; - if (avatar_) - avatar_->setImage(img); + avatar_->setImage(QString::fromStdString(info_.avatar_url)); } void @@ -644,8 +741,10 @@ RoomSettings::resetErrorLabel() void RoomSettings::updateAvatar() { - const auto fileName = - QFileDialog::getOpenFileName(this, tr("Select an avatar"), "", tr("All Files (*)")); + const QString picturesFolder = + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + const QString fileName = QFileDialog::getOpenFileName( + this, tr("Select an avatar"), picturesFolder, tr("All Files (*)")); if (fileName.isEmpty()) return; @@ -657,12 +756,12 @@ RoomSettings::updateAvatar() QFile file{fileName, this}; if (format != "image") { - displayErrorMessage(tr("The selected media is not an image")); + displayErrorMessage(tr("The selected file is not an image")); return; } if (!file.open(QIODevice::ReadOnly)) { - displayErrorMessage(tr("Error while reading media: %1").arg(file.errorString())); + displayErrorMessage(tr("Error while reading file: %1").arg(file.errorString())); return; } @@ -722,7 +821,7 @@ RoomSettings::updateAvatar() return; } - emit proxy->avatarChanged(QImage::fromData(content)); + emit proxy->avatarChanged(); }); }); } diff --git a/src/dialogs/RoomSettings.h b/src/dialogs/RoomSettings.h index 6667b68b..e41c866c 100644 --- a/src/dialogs/RoomSettings.h +++ b/src/dialogs/RoomSettings.h @@ -5,7 +5,9 @@ #include <QImage> #include <QLabel> -#include "Cache.h" +#include <mtx/events/guest_access.hpp> + +#include "CacheStructs.h" class Avatar; class FlatButton; @@ -33,7 +35,7 @@ signals: void clicked(); protected: - bool eventFilter(QObject *obj, QEvent *event) + bool eventFilter(QObject *obj, QEvent *event) override { if (event->type() == QEvent::MouseButtonRelease) { emit clicked(); @@ -52,7 +54,7 @@ class ThreadProxy : public QObject signals: void error(const QString &msg); - void avatarChanged(const QImage &img); + void avatarChanged(); void nameEventSent(const QString &); void topicEventSent(); }; @@ -117,6 +119,7 @@ signals: void enableEncryptionError(const QString &msg); void showErrorMessage(const QString &msg); void accessRulesUpdated(); + void notifChanged(int index); protected: void showEvent(QShowEvent *event) override; @@ -140,7 +143,7 @@ private: void resetErrorLabel(); void displayErrorMessage(const QString &msg); - void setAvatar(const QImage &img); + void setAvatar(); void setupEditButton(); //! Retrieve the current room information from cache. void retrieveRoomInfo(); @@ -161,6 +164,7 @@ private: QLabel *errorLabel_ = nullptr; LoadingIndicator *spinner_ = nullptr; + QComboBox *notifCombo = nullptr; QComboBox *accessCombo = nullptr; Toggle *encryptionToggle_ = nullptr; Toggle *keyRequestsToggle_ = nullptr; diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp index b8040f9f..f1dd77df 100644 --- a/src/dialogs/UserProfile.cpp +++ b/src/dialogs/UserProfile.cpp @@ -1,13 +1,12 @@ #include <QHBoxLayout> #include <QLabel> #include <QListWidget> -#include <QSettings> #include <QShortcut> #include <QVBoxLayout> -#include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" +#include "Logging.h" #include "MatrixClient.h" #include "Utils.h" #include "dialogs/UserProfile.h" @@ -16,6 +15,8 @@ using namespace dialogs; +Q_DECLARE_METATYPE(std::vector<DeviceInfo>) + constexpr int BUTTON_SIZE = 36; constexpr int BUTTON_RADIUS = BUTTON_SIZE / 2; constexpr int WIDGET_MARGIN = 20; @@ -49,7 +50,6 @@ UserProfile::UserProfile(QWidget *parent) { setAutoFillBackground(true); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); setAttribute(Qt::WA_DeleteOnClose, true); QIcon banIcon, kickIcon, ignoreIcon, startChatIcon; @@ -61,7 +61,6 @@ UserProfile::UserProfile(QWidget *parent) banBtn_->setIcon(banIcon); banBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS)); banBtn_->setToolTip(tr("Ban the user from the room")); - banBtn_->setDisabled(true); // Not used yet. ignoreIcon.addFile(":/icons/icons/ui/volume-off-indicator.png"); ignoreBtn_ = new FlatButton(this); @@ -79,7 +78,6 @@ UserProfile::UserProfile(QWidget *parent) kickBtn_->setIcon(kickIcon); kickBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS)); kickBtn_->setToolTip(tr("Kick the user from the room")); - kickBtn_->setDisabled(true); // Not used yet. startChatIcon.addFile(":/icons/icons/ui/black-bubble-speech.png"); startChat_ = new FlatButton(this); @@ -102,6 +100,13 @@ UserProfile::UserProfile(QWidget *parent) emit ChatPage::instance()->createRoom(req); }); + connect(banBtn_, &QPushButton::clicked, this, [this] { + ChatPage::instance()->banUser(userIdLabel_->text(), ""); + }); + connect(kickBtn_, &QPushButton::clicked, this, [this] { + ChatPage::instance()->kickUser(userIdLabel_->text(), ""); + }); + // Button line auto btnLayout = new QHBoxLayout; btnLayout->addStretch(1); @@ -114,9 +119,8 @@ UserProfile::UserProfile(QWidget *parent) btnLayout->setSpacing(8); btnLayout->setMargin(0); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, 128); avatar_->setLetter("X"); - avatar_->setSize(128); QFont font; font.setPointSizeF(font.pointSizeF() * 2); @@ -167,10 +171,6 @@ UserProfile::UserProfile(QWidget *parent) vlayout->setAlignment(avatar_, Qt::AlignCenter | Qt::AlignTop); vlayout->setAlignment(userIdLabel_, Qt::AlignCenter | Qt::AlignTop); - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - QFont largeFont; largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); @@ -181,9 +181,10 @@ UserProfile::UserProfile(QWidget *parent) vlayout->setSpacing(WIDGET_SPACING); vlayout->setContentsMargins(WIDGET_MARGIN, TOP_WIDGET_MARGIN, WIDGET_MARGIN, WIDGET_MARGIN); - qRegisterMetaType<std::vector<DeviceInfo>>(); + static auto ignored = qRegisterMetaType<std::vector<DeviceInfo>>(); + (void)ignored; - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &UserProfile::close); connect(okBtn, &QPushButton::clicked, this, &UserProfile::close); } @@ -204,22 +205,21 @@ UserProfile::init(const QString &userId, const QString &roomId) { resetToDefaults(); - auto displayName = Cache::displayName(roomId, userId); + auto displayName = cache::displayName(roomId, userId); userIdLabel_->setText(userId); displayNameLabel_->setText(displayName); avatar_->setLetter(utils::firstChar(displayName)); - AvatarProvider::resolve( - roomId, userId, this, [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(roomId, userId); auto localUser = utils::localUser(); try { bool hasMemberRights = - cache::client()->hasEnoughPowerLevel({mtx::events::EventType::RoomMember}, - roomId.toStdString(), - localUser.toStdString()); + cache::hasEnoughPowerLevel({mtx::events::EventType::RoomMember}, + roomId.toStdString(), + localUser.toStdString()); if (!hasMemberRights) { kickBtn_->hide(); banBtn_->hide(); diff --git a/src/dialogs/UserProfile.h b/src/dialogs/UserProfile.h index 0f684cda..81276d2a 100644 --- a/src/dialogs/UserProfile.h +++ b/src/dialogs/UserProfile.h @@ -15,8 +15,6 @@ struct DeviceInfo QString display_name; }; -Q_DECLARE_METATYPE(std::vector<DeviceInfo>) - class Proxy : public QObject { Q_OBJECT diff --git a/src/emoji/Category.cpp b/src/emoji/Category.cpp index fbfbf4fc..e674e9db 100644 --- a/src/emoji/Category.cpp +++ b/src/emoji/Category.cpp @@ -43,15 +43,18 @@ Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent) emojiListView_->setViewMode(QListView::IconMode); emojiListView_->setFlow(QListView::LeftToRight); emojiListView_->setResizeMode(QListView::Adjust); + emojiListView_->setMouseTracking(true); emojiListView_->verticalScrollBar()->setEnabled(false); emojiListView_->horizontalScrollBar()->setEnabled(false); const int cols = 7; - const int rows = emoji.size() / 7; + const int rows = emoji.size() / 7 + 1; + const int emojiSize = 48; + const int gridSize = emojiSize + 4; // TODO: Be precise here. Take the parent into consideration. - emojiListView_->setFixedSize(cols * 50 + 20, rows * 50 + 20); - emojiListView_->setGridSize(QSize(50, 50)); + emojiListView_->setFixedSize(cols * gridSize + 20, rows * gridSize); + emojiListView_->setGridSize(QSize(gridSize, gridSize)); emojiListView_->setDragEnabled(false); emojiListView_->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -59,7 +62,7 @@ Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent) data_->unicode = e.unicode; auto item = new QStandardItem; - item->setSizeHint(QSize(24, 24)); + item->setSizeHint(QSize(emojiSize, emojiSize)); QVariant unicode(data_->unicode); item->setData(unicode.toString(), Qt::UserRole); @@ -72,7 +75,6 @@ Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent) category_ = new QLabel(category, this); category_->setFont(font); - category_->setStyleSheet("margin: 20px 0 20px 8px;"); mainLayout_->addWidget(category_); mainLayout_->addWidget(emojiListView_); diff --git a/src/emoji/Category.h b/src/emoji/Category.h index a14029c8..2f39d621 100644 --- a/src/emoji/Category.h +++ b/src/emoji/Category.h @@ -17,6 +17,7 @@ #pragma once +#include <QColor> #include <QLabel> #include <QLayout> #include <QListView> @@ -29,9 +30,13 @@ namespace emoji { class Category : public QWidget { Q_OBJECT + Q_PROPERTY( + QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) public: Category(QString category, std::vector<Emoji> emoji, QWidget *parent = nullptr); + QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } + void setHoverBackgroundColor(QColor color) { hoverBackgroundColor_ = color; } signals: void emojiSelected(const QString &emoji); @@ -55,5 +60,7 @@ private: emoji::ItemDelegate *delegate_; QLabel *category_; + + QColor hoverBackgroundColor_; }; } // namespace emoji diff --git a/src/emoji/ItemDelegate.cpp b/src/emoji/ItemDelegate.cpp index b79ae0fc..afa01625 100644 --- a/src/emoji/ItemDelegate.cpp +++ b/src/emoji/ItemDelegate.cpp @@ -15,8 +15,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QDebug> #include <QPainter> +#include <QSettings> #include "emoji/ItemDelegate.h" @@ -37,12 +37,30 @@ ItemDelegate::paint(QPainter *painter, { Q_UNUSED(index); + painter->save(); + QStyleOptionViewItem viewOption(option); auto emoji = index.data(Qt::UserRole).toString(); - // QFont font("Emoji One"); + QSettings settings; + QFont font; + QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); + if (!userFontFamily.isEmpty()) { + font.setFamily(userFontFamily); + } else { + font.setFamily("emoji"); + } + + font.setPixelSize(36); painter->setFont(font); + if (option.state & QStyle::State_MouseOver) { + painter->setBackgroundMode(Qt::OpaqueMode); + QColor hoverColor = parent()->property("hoverBackgroundColor").value<QColor>(); + painter->setBackground(hoverColor); + } painter->drawText(viewOption.rect, Qt::AlignCenter, emoji); + + painter->restore(); } diff --git a/src/emoji/ItemDelegate.h b/src/emoji/ItemDelegate.h index e0456308..d6b9b9d7 100644 --- a/src/emoji/ItemDelegate.h +++ b/src/emoji/ItemDelegate.h @@ -31,7 +31,7 @@ class ItemDelegate : public QStyledItemDelegate public: explicit ItemDelegate(QObject *parent = nullptr); - ~ItemDelegate(); + ~ItemDelegate() override; void paint(QPainter *painter, const QStyleOptionViewItem &option, diff --git a/src/emoji/Panel.cpp b/src/emoji/Panel.cpp index 710b501e..f0e4449d 100644 --- a/src/emoji/Panel.cpp +++ b/src/emoji/Panel.cpp @@ -15,6 +15,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QPainter> #include <QPushButton> #include <QScrollBar> #include <QVBoxLayout> @@ -34,10 +35,6 @@ Panel::Panel(QWidget *parent) , 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); @@ -202,14 +199,12 @@ Panel::showCategory(const Category *category) 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. + // We want the top of the category to be visible, so scroll to the top first and then to the + // category if (current > posToGo) this->scrollArea_->ensureVisible(0, 0, 0, 0); - posToGo += 6 * 50; + posToGo += scrollArea_->height(); this->scrollArea_->ensureVisible(0, posToGo, 0, 0); } diff --git a/src/emoji/Provider.cpp b/src/emoji/Provider.cpp index f7b8dab9..4ed8bd71 100644 --- a/src/emoji/Provider.cpp +++ b/src/emoji/Provider.cpp @@ -15,1383 +15,4702 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QByteArray> - #include "emoji/Provider.h" using namespace emoji; -const std::vector<Emoji> Provider::people = { - Emoji{QString::fromUtf8("\xf0\x9f\x98\x80"), ":grinning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x81"), ":grin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x82"), ":joy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa3"), ":rofl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x83"), ":smiley:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x84"), ":smile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x85"), ":sweat_smile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x86"), ":laughing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x89"), ":wink:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8a"), ":blush:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8b"), ":yum:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8e"), ":sunglasses:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8d"), ":heart_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x98"), ":kissing_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x97"), ":kissing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x99"), ":kissing_smiling_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9a"), ":kissing_closed_eyes:"}, - Emoji{QString::fromUtf8("\xe2\x98\xba"), ":relaxed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x82"), ":slight_smile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x97"), ":hugging:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x94"), ":thinking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x90"), ":neutral_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x91"), ":expressionless:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb6"), ":no_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x84"), ":rolling_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8f"), ":smirk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa3"), ":persevere:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa5"), ":disappointed_relieved:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xae"), ":open_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x90"), ":zipper_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xaf"), ":hushed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xaa"), ":sleepy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xab"), ":tired_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb4"), ":sleeping:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8c"), ":relieved:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x93"), ":nerd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9b"), ":stuck_out_tongue:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9c"), ":stuck_out_tongue_winking_eye:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9d"), ":stuck_out_tongue_closed_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa4"), ":drooling_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x92"), ":unamused:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x93"), ":sweat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x94"), ":pensive:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x95"), ":confused:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x83"), ":upside_down:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x91"), ":money_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb2"), ":astonished:"}, - Emoji{QString::fromUtf8("\xe2\x98\xb9"), ":frowning2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x81"), ":slight_frown:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x96"), ":confounded:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9e"), ":disappointed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9f"), ":worried:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa4"), ":triumph:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa2"), ":cry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xad"), ":sob:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa6"), ":frowning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa7"), ":anguished:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa8"), ":fearful:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa9"), ":weary:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xac"), ":grimacing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb0"), ":cold_sweat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb1"), ":scream:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb3"), ":flushed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5"), ":dizzy_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa1"), ":rage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa0"), ":angry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x87"), ":innocent:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa0"), ":cowboy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa1"), ":clown:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa5"), ":lying_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb7"), ":mask:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x92"), ":thermometer_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x95"), ":head_bandage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa2"), ":nauseated_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa7"), ":sneezing_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x88"), ":smiling_imp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbf"), ":imp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb9"), ":japanese_ogre:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xba"), ":japanese_goblin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x80"), ":skull:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbb"), ":ghost:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbd"), ":alien:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x96"), ":robot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa9"), ":poop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xba"), ":smiley_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb8"), ":smile_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb9"), ":joy_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbb"), ":heart_eyes_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbc"), ":smirk_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbd"), ":kissing_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x80"), ":scream_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbf"), ":crying_cat_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbe"), ":pouting_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6"), ":boy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7"), ":girl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8"), ":man:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9"), ":woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4"), ":older_man:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5"), ":older_woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6"), ":baby:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc"), ":angel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xae"), ":cop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5"), ":spy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x82"), ":guardsman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7"), ":construction_worker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3"), ":man_with_turban:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1"), ":person_with_blond_hair:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85"), ":santa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6"), ":mrs_claus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8"), ":princess:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4"), ":prince:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0"), ":bride_with_veil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5"), ":man_in_tuxedo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0"), ":pregnant_woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2"), ":man_with_gua_pi_mao:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d"), ":person_frowning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e"), ":person_with_pouting_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x85"), ":no_good:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x86"), ":ok_woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x81"), ":information_desk_person:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b"), ":raising_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x87"), ":bow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6"), ":face_palm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7"), ":shrug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x86"), ":massage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x87"), ":haircut:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6"), ":walking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83"), ":runner:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x83"), ":dancer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xba"), ":man_dancing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf"), ":dancers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xa3"), ":speaking_head:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa4"), ":bust_in_silhouette:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa5"), ":busts_in_silhouette:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xab"), ":couple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xac"), ":two_men_holding_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xad"), ":two_women_holding_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8f"), ":couplekiss:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x91"), ":couple_with_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xaa"), ":family:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa"), ":muscle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3"), ":selfie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x88"), ":point_left:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x89"), ":point_right:"}, - Emoji{QString::fromUtf8("\xe2\x98\x9d"), ":point_up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x86"), ":point_up_2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x95"), ":middle_finger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x87"), ":point_down:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8c"), ":v:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e"), ":fingers_crossed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x96"), ":vulcan:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98"), ":metal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99"), ":call_me:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x90"), ":hand_splayed:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8b"), ":raised_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c"), ":ok_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d"), ":thumbsup:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e"), ":thumbsdown:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8a"), ":fist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a"), ":punch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b"), ":left_facing_fist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c"), ":right_facing_fist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a"), ":raised_back_of_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b"), ":wave:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f"), ":clap:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8d"), ":writing_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x90"), ":open_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c"), ":raised_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f"), ":pray:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d"), ":handshake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x85"), ":nail_care:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x82"), ":ear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x83"), ":nose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa3"), ":footprints:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x80"), ":eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x81"), ":eye:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x85"), ":tongue:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x84"), ":lips:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8b"), ":kiss:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa4"), ":zzz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x93"), ":eyeglasses:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb6"), ":dark_sunglasses:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x94"), ":necktie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x95"), ":shirt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x96"), ":jeans:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x97"), ":dress:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x98"), ":kimono:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x99"), ":bikini:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9a"), ":womans_clothes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9b"), ":purse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9c"), ":handbag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9d"), ":pouch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x92"), ":school_satchel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9e"), ":mans_shoe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9f"), ":athletic_shoe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa0"), ":high_heel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa1"), ":sandal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa2"), ":boot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x91"), ":crown:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x92"), ":womans_hat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa9"), ":tophat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x93"), ":mortar_board:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x91"), ":helmet_with_cross:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x84"), ":lipstick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8d"), ":ring:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x82"), ":closed_umbrella:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbc"), ":briefcase:"}, +const std::vector<Emoji> emoji::Provider::people = { + Emoji{QString::fromUtf8("\xf0\x9f\x98\x80"), "grinning face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x83"), "grinning face with big eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x84"), "grinning face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x81"), "beaming face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x86"), "grinning squinting face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x85"), "grinning face with sweat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa3"), "rolling on the floor laughing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x82"), "face with tears of joy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x82"), "slightly smiling face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x83"), "upside-down face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x89"), "winking face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8a"), "smiling face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x87"), "smiling face with halo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb0"), "smiling face with hearts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8d"), "smiling face with heart-eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa9"), "star-struck"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x98"), "face blowing a kiss"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x97"), "kissing face"}, + Emoji{QString::fromUtf8("\xe2\x98\xba"), "smiling face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9a"), "kissing face with closed eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x99"), "kissing face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb2"), "smiling face with tear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8b"), "face savoring food"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9b"), "face with tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9c"), "winking face with tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xaa"), "zany face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9d"), "squinting face with tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x91"), "money-mouth face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x97"), "hugging face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xad"), "face with hand over mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xab"), "shushing face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x94"), "thinking face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x90"), "zipper-mouth face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa8"), "face with raised eyebrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x90"), "neutral face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x91"), "expressionless face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb6"), "face without mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8f"), "smirking face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x92"), "unamused face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x84"), "face with rolling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xac"), "grimacing face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa5"), "lying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8c"), "relieved face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x94"), "pensive face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xaa"), "sleepy face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa4"), "drooling face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb4"), "sleeping face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb7"), "face with medical mask"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x92"), "face with thermometer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x95"), "face with head-bandage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa2"), "nauseated face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xae"), "face vomiting"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa7"), "sneezing face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb5"), "hot face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb6"), "cold face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb4"), "woozy face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5"), "dizzy face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xaf"), "exploding head"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa0"), "cowboy hat face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb3"), "partying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb8"), "disguised face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8e"), "smiling face with sunglasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x93"), "nerd face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x90"), "face with monocle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x95"), "confused face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9f"), "worried face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x81"), "slightly frowning face"}, + Emoji{QString::fromUtf8("\xe2\x98\xb9"), "frowning face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xae"), "face with open mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xaf"), "hushed face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb2"), "astonished face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb3"), "flushed face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xba"), "pleading face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa6"), "frowning face with open mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa7"), "anguished face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa8"), "fearful face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb0"), "anxious face with sweat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa5"), "sad but relieved face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa2"), "crying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xad"), "loudly crying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb1"), "face screaming in fear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x96"), "confounded face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa3"), "persevering face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9e"), "disappointed face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x93"), "downcast face with sweat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa9"), "weary face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xab"), "tired face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb1"), "yawning face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa4"), "face with steam from nose"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa1"), "pouting face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa0"), "angry face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xac"), "face with symbols on mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x88"), "smiling face with horns"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbf"), "angry face with horns"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x80"), "skull"}, + Emoji{QString::fromUtf8("\xe2\x98\xa0"), "skull and crossbones"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa9"), "pile of poo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa1"), "clown face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb9"), "ogre"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xba"), "goblin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbb"), "ghost"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbd"), "alien"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbe"), "alien monster"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x96"), "robot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xba"), "grinning cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb8"), "grinning cat with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb9"), "cat with tears of joy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbb"), "smiling cat with heart-eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbc"), "cat with wry smile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbd"), "kissing cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x80"), "weary cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbf"), "crying cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbe"), "pouting cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x88"), "see-no-evil monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x89"), "hear-no-evil monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8a"), "speak-no-evil monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8b"), "kiss mark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8c"), "love letter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x98"), "heart with arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9d"), "heart with ribbon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x96"), "sparkling heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x97"), "growing heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x93"), "beating heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9e"), "revolving hearts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x95"), "two hearts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9f"), "heart decoration"}, + Emoji{QString::fromUtf8("\xe2\x9d\xa3"), "heart exclamation"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x94"), "broken heart"}, + Emoji{QString::fromUtf8("\xe2\x9d\xa4"), "red heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa1"), "orange heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9b"), "yellow heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9a"), "green heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x99"), "blue heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9c"), "purple heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8e"), "brown heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xa4"), "black heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8d"), "white heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaf"), "hundred points"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa2"), "anger symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa5"), "collision"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xab"), "dizzy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa6"), "sweat droplets"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa8"), "dashing away"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb3"), "hole"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa3"), "bomb"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xac"), "speech balloon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x81\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x97\xa8"), + "eye in speech bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x81\xe2\x80\x8d\xf0\x9f\x97\xa8"), "eye in speech bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xa8"), "left speech bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xaf"), "right anger bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xad"), "thought balloon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa4"), "zzz"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b"), "waving hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbb"), "waving hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbc"), + "waving hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbd"), "waving hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbe"), + "waving hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbf"), "waving hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a"), "raised back of hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbb"), + "raised back of hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbc"), + "raised back of hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbd"), + "raised back of hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbe"), + "raised back of hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbf"), + "raised back of hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90"), "hand with fingers splayed"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbb"), + "hand with fingers splayed: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbc"), + "hand with fingers splayed: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbd"), + "hand with fingers splayed: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbe"), + "hand with fingers splayed: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbf"), + "hand with fingers splayed: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b"), "raised hand"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbb"), "raised hand: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbc"), "raised hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbd"), "raised hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbe"), "raised hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbf"), "raised hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96"), "vulcan salute"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbb"), "vulcan salute: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbc"), + "vulcan salute: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbd"), "vulcan salute: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbe"), + "vulcan salute: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbf"), "vulcan salute: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c"), "OK hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbb"), "OK hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbc"), "OK hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbd"), "OK hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbe"), "OK hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbf"), "OK hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c"), "pinched fingers"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbb"), "pinched fingers: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbc"), + "pinched fingers: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbd"), "pinched fingers: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbe"), + "pinched fingers: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbf"), "pinched fingers: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f"), "pinching hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbb"), "pinching hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbc"), + "pinching hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbd"), "pinching hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbe"), + "pinching hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbf"), "pinching hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c"), "victory hand"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbb"), "victory hand: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbc"), "victory hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbd"), "victory hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbe"), "victory hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbf"), "victory hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e"), "crossed fingers"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbb"), "crossed fingers: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbc"), + "crossed fingers: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbd"), "crossed fingers: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbe"), + "crossed fingers: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbf"), "crossed fingers: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f"), "love-you gesture"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbb"), "love-you gesture: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbc"), + "love-you gesture: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbd"), + "love-you gesture: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbe"), + "love-you gesture: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbf"), "love-you gesture: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98"), "sign of the horns"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbb"), + "sign of the horns: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbc"), + "sign of the horns: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbd"), + "sign of the horns: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbe"), + "sign of the horns: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbf"), "sign of the horns: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99"), "call me hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbb"), "call me hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbc"), + "call me hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbd"), "call me hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbe"), + "call me hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbf"), "call me hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88"), "backhand index pointing left"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbb"), + "backhand index pointing left: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbc"), + "backhand index pointing left: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbd"), + "backhand index pointing left: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbe"), + "backhand index pointing left: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbf"), + "backhand index pointing left: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89"), "backhand index pointing right"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbb"), + "backhand index pointing right: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbc"), + "backhand index pointing right: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbd"), + "backhand index pointing right: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbe"), + "backhand index pointing right: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbf"), + "backhand index pointing right: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86"), "backhand index pointing up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbb"), + "backhand index pointing up: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbc"), + "backhand index pointing up: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbd"), + "backhand index pointing up: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbe"), + "backhand index pointing up: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbf"), + "backhand index pointing up: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95"), "middle finger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbb"), "middle finger: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbc"), + "middle finger: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbd"), "middle finger: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbe"), + "middle finger: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbf"), "middle finger: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87"), "backhand index pointing down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbb"), + "backhand index pointing down: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbc"), + "backhand index pointing down: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbd"), + "backhand index pointing down: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbe"), + "backhand index pointing down: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbf"), + "backhand index pointing down: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d"), "index pointing up"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbb"), "index pointing up: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbc"), + "index pointing up: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbd"), "index pointing up: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbe"), + "index pointing up: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbf"), "index pointing up: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d"), "thumbs up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbb"), "thumbs up: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbc"), "thumbs up: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbd"), "thumbs up: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbe"), "thumbs up: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbf"), "thumbs up: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e"), "thumbs down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbb"), "thumbs down: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbc"), + "thumbs down: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbd"), "thumbs down: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbe"), + "thumbs down: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbf"), "thumbs down: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a"), "raised fist"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbb"), "raised fist: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbc"), "raised fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbd"), "raised fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbe"), "raised fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbf"), "raised fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a"), "oncoming fist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbb"), "oncoming fist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbc"), + "oncoming fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbd"), "oncoming fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbe"), + "oncoming fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbf"), "oncoming fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b"), "left-facing fist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbb"), "left-facing fist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbc"), + "left-facing fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbd"), + "left-facing fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbe"), + "left-facing fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbf"), "left-facing fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c"), "right-facing fist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbb"), + "right-facing fist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbc"), + "right-facing fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbd"), + "right-facing fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbe"), + "right-facing fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbf"), "right-facing fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f"), "clapping hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbb"), "clapping hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbc"), + "clapping hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbd"), "clapping hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbe"), + "clapping hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbf"), "clapping hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c"), "raising hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbb"), "raising hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbc"), + "raising hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbd"), "raising hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbe"), + "raising hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbf"), "raising hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90"), "open hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbb"), "open hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbc"), + "open hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbd"), "open hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbe"), "open hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbf"), "open hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2"), "palms up together"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbb"), + "palms up together: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbc"), + "palms up together: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbd"), + "palms up together: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbe"), + "palms up together: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbf"), "palms up together: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d"), "handshake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f"), "folded hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbb"), "folded hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbc"), + "folded hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbd"), "folded hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbe"), + "folded hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbf"), "folded hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d"), "writing hand"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbb"), "writing hand: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbc"), "writing hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbd"), "writing hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbe"), "writing hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbf"), "writing hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85"), "nail polish"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbb"), "nail polish: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbc"), + "nail polish: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbd"), "nail polish: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbe"), + "nail polish: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbf"), "nail polish: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3"), "selfie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbb"), "selfie: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbc"), "selfie: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbd"), "selfie: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbe"), "selfie: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbf"), "selfie: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa"), "flexed biceps"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbb"), "flexed biceps: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbc"), + "flexed biceps: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbd"), "flexed biceps: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbe"), + "flexed biceps: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbf"), "flexed biceps: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbe"), "mechanical arm"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbf"), "mechanical leg"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5"), "leg"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbb"), "leg: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbc"), "leg: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbd"), "leg: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbe"), "leg: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbf"), "leg: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6"), "foot"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbb"), "foot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbc"), "foot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbd"), "foot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbe"), "foot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbf"), "foot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82"), "ear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbb"), "ear: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbc"), "ear: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbd"), "ear: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbe"), "ear: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbf"), "ear: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb"), "ear with hearing aid"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbb"), + "ear with hearing aid: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbc"), + "ear with hearing aid: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbd"), + "ear with hearing aid: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbe"), + "ear with hearing aid: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbf"), + "ear with hearing aid: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83"), "nose"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbb"), "nose: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbc"), "nose: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbd"), "nose: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbe"), "nose: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbf"), "nose: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa0"), "brain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x80"), "anatomical heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x81"), "lungs"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb7"), "tooth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb4"), "bone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x80"), "eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x81"), "eye"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x85"), "tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x84"), "mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6"), "baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbb"), "baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbc"), "baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbd"), "baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbe"), "baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbf"), "baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92"), "child"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbb"), "child: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbc"), "child: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbd"), "child: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbe"), "child: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbf"), "child: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6"), "boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbb"), "boy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbc"), "boy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbd"), "boy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbe"), "boy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbf"), "boy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7"), "girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbb"), "girl: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbc"), "girl: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbd"), "girl: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbe"), "girl: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbf"), "girl: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91"), "person"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), "person: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), "person: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), "person: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), "person: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), "person: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1"), "person: blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb"), + "person: light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc"), + "person: medium-light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd"), + "person: medium skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe"), + "person: medium-dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf"), + "person: dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8"), "man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), "man: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), "man: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), "man: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), "man: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), "man: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94"), "man: beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbb"), "man: light skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbc"), + "man: medium-light skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbd"), "man: medium skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbe"), "man: medium-dark skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbf"), "man: dark skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb0"), "man: red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: medium-light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: medium skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: medium-dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb1"), "man: curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: medium-light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: medium skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: medium-dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb3"), "man: white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: medium-light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: medium skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: medium-dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb2"), "man: bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: medium-light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: medium skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: medium-dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9"), "woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), "woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), "woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), "woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), "woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), "woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb0"), "woman: red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: medium-light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: medium skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: medium-dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb0"), "person: red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: medium-light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: medium skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: medium-dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb1"), "woman: curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: medium-light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: medium skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: medium-dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb1"), "person: curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: medium-light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: medium skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: medium-dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb3"), "woman: white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: medium-light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: medium skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: medium-dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb3"), "person: white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: medium-light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: medium skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: medium-dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb2"), "woman: bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: medium-light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: medium skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: medium-dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb2"), "person: bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: medium-light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: medium skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: medium-dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xe2\x80\x8d\xe2\x99\x80"), "woman: blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman: light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman: medium-light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman: medium skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman: medium-dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman: dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xe2\x80\x8d\xe2\x99\x82"), "man: blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man: light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man: medium-light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man: medium skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man: medium-dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man: dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93"), "older person"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbb"), "older person: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbc"), + "older person: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbd"), "older person: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbe"), + "older person: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbf"), "older person: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4"), "old man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbb"), "old man: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbc"), "old man: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbd"), "old man: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbe"), "old man: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbf"), "old man: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5"), "old woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbb"), "old woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbc"), "old woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbd"), "old woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbe"), "old woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbf"), "old woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d"), "person frowning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb"), "person frowning: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc"), + "person frowning: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd"), "person frowning: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe"), + "person frowning: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf"), "person frowning: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xe2\x80\x8d\xe2\x99\x82"), "man frowning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xe2\x80\x8d\xe2\x99\x80"), "woman frowning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e"), "person pouting"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb"), "person pouting: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc"), + "person pouting: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd"), "person pouting: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe"), + "person pouting: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf"), "person pouting: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xe2\x80\x8d\xe2\x99\x82"), "man pouting"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xe2\x80\x8d\xe2\x99\x80"), "woman pouting"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85"), "person gesturing NO"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb"), + "person gesturing NO: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc"), + "person gesturing NO: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd"), + "person gesturing NO: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe"), + "person gesturing NO: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf"), + "person gesturing NO: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xe2\x80\x8d\xe2\x99\x82"), "man gesturing NO"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xe2\x80\x8d\xe2\x99\x80"), "woman gesturing NO"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86"), "person gesturing OK"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb"), + "person gesturing OK: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc"), + "person gesturing OK: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd"), + "person gesturing OK: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe"), + "person gesturing OK: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf"), + "person gesturing OK: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xe2\x80\x8d\xe2\x99\x82"), "man gesturing OK"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xe2\x80\x8d\xe2\x99\x80"), "woman gesturing OK"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81"), "person tipping hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb"), + "person tipping hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc"), + "person tipping hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd"), + "person tipping hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe"), + "person tipping hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf"), + "person tipping hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xe2\x80\x8d\xe2\x99\x82"), "man tipping hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xe2\x80\x8d\xe2\x99\x80"), "woman tipping hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b"), "person raising hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb"), + "person raising hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc"), + "person raising hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd"), + "person raising hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe"), + "person raising hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf"), + "person raising hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xe2\x80\x8d\xe2\x99\x82"), "man raising hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xe2\x80\x8d\xe2\x99\x80"), "woman raising hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f"), "deaf person"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb"), "deaf person: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc"), + "deaf person: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd"), "deaf person: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe"), + "deaf person: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf"), "deaf person: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xe2\x80\x8d\xe2\x99\x82"), "deaf man"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xe2\x80\x8d\xe2\x99\x80"), "deaf woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87"), "person bowing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb"), "person bowing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc"), + "person bowing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd"), "person bowing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe"), + "person bowing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf"), "person bowing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x82"), "man bowing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x80"), "woman bowing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6"), "person facepalming"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb"), + "person facepalming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc"), + "person facepalming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd"), + "person facepalming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe"), + "person facepalming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf"), + "person facepalming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xe2\x80\x8d\xe2\x99\x82"), "man facepalming"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xe2\x80\x8d\xe2\x99\x80"), "woman facepalming"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7"), "person shrugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb"), "person shrugging: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc"), + "person shrugging: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd"), + "person shrugging: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe"), + "person shrugging: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf"), "person shrugging: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xe2\x80\x8d\xe2\x99\x82"), "man shrugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xe2\x80\x8d\xe2\x99\x80"), "woman shrugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9a\x95"), "health worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9a\x95"), "man health worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9a\x95"), "woman health worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\x93"), "student"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\x93"), "man student"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\x93"), "woman student"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8f\xab"), "teacher"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8f\xab"), "man teacher"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8f\xab"), "woman teacher"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9a\x96"), "judge"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96"), + "judge: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96"), + "judge: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96"), + "judge: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96"), + "judge: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96"), + "judge: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9a\x96"), "man judge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9a\x96"), "woman judge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8c\xbe"), "farmer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8c\xbe"), "man farmer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8c\xbe"), "woman farmer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8d\xb3"), "cook"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8d\xb3"), "man cook"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8d\xb3"), "woman cook"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x94\xa7"), "mechanic"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x94\xa7"), "man mechanic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x94\xa7"), "woman mechanic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8f\xad"), "factory worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8f\xad"), "man factory worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8f\xad"), "woman factory worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x92\xbc"), "office worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbc"), "man office worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x92\xbc"), "woman office worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x94\xac"), "scientist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x94\xac"), "man scientist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x94\xac"), "woman scientist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x92\xbb"), "technologist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbb"), "man technologist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x92\xbb"), "woman technologist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\xa4"), "singer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\xa4"), "man singer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\xa4"), "woman singer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\xa8"), "artist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\xa8"), "man artist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\xa8"), "woman artist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9c\x88"), "pilot"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9c\x88"), "man pilot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9c\x88"), "woman pilot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x9a\x80"), "astronaut"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x9a\x80"), "man astronaut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x9a\x80"), "woman astronaut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x9a\x92"), "firefighter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x9a\x92"), "man firefighter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x9a\x92"), "woman firefighter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae"), "police officer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb"), "police officer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc"), + "police officer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd"), "police officer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe"), + "police officer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf"), "police officer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xe2\x80\x8d\xe2\x99\x82"), "man police officer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xe2\x80\x8d\xe2\x99\x80"), "woman police officer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5"), "detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb"), "detective: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc"), "detective: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd"), "detective: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe"), "detective: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf"), "detective: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), "man detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xe2\x80\x8d\xe2\x99\x82"), "man detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man detective: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man detective: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man detective: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man detective: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man detective: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), + "woman detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xe2\x80\x8d\xe2\x99\x80"), "woman detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82"), "guard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb"), "guard: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc"), "guard: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd"), "guard: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe"), "guard: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf"), "guard: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xe2\x80\x8d\xe2\x99\x82"), "man guard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man guard: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man guard: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man guard: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man guard: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man guard: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xe2\x80\x8d\xe2\x99\x80"), "woman guard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7"), "construction worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb"), + "construction worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc"), + "construction worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd"), + "construction worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe"), + "construction worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf"), + "construction worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xe2\x80\x8d\xe2\x99\x82"), "man construction worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xe2\x80\x8d\xe2\x99\x80"), "woman construction worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4"), "prince"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbb"), "prince: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbc"), "prince: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbd"), "prince: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbe"), "prince: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbf"), "prince: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8"), "princess"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbb"), "princess: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbc"), "princess: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbd"), "princess: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbe"), "princess: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbf"), "princess: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3"), "person wearing turban"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb"), + "person wearing turban: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc"), + "person wearing turban: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd"), + "person wearing turban: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe"), + "person wearing turban: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf"), + "person wearing turban: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xe2\x80\x8d\xe2\x99\x82"), "man wearing turban"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xe2\x80\x8d\xe2\x99\x80"), "woman wearing turban"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2"), "man with skullcap"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbb"), + "man with skullcap: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbc"), + "man with skullcap: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbd"), + "man with skullcap: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbe"), + "man with skullcap: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbf"), "man with skullcap: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95"), "woman with headscarf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbb"), + "woman with headscarf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbc"), + "woman with headscarf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbd"), + "woman with headscarf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbe"), + "woman with headscarf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbf"), + "woman with headscarf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5"), "man in tuxedo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb"), "man in tuxedo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc"), + "man in tuxedo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd"), "man in tuxedo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe"), + "man in tuxedo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf"), "man in tuxedo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xe2\x80\x8d\xe2\x99\x82"), "man in tuxedo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xe2\x80\x8d\xe2\x99\x80"), "woman in tuxedo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0"), "bride with veil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb"), "bride with veil: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc"), + "bride with veil: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd"), "bride with veil: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe"), + "bride with veil: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf"), "bride with veil: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xe2\x80\x8d\xe2\x99\x82"), "man with veil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xe2\x80\x8d\xe2\x99\x80"), "woman with veil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0"), "pregnant woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbb"), "pregnant woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbc"), + "pregnant woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbd"), "pregnant woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbe"), + "pregnant woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbf"), "pregnant woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1"), "breast-feeding"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbb"), "breast-feeding: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbc"), + "breast-feeding: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbd"), "breast-feeding: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbe"), + "breast-feeding: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbf"), "breast-feeding: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8d\xbc"), "woman feeding baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8d\xbc"), "man feeding baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8d\xbc"), "person feeding baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc"), "baby angel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbb"), "baby angel: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbc"), + "baby angel: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbd"), "baby angel: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbe"), "baby angel: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbf"), "baby angel: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85"), "Santa Claus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbb"), "Santa Claus: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbc"), + "Santa Claus: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbd"), "Santa Claus: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbe"), + "Santa Claus: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbf"), "Santa Claus: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6"), "Mrs. Claus"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbb"), "Mrs. Claus: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbc"), + "Mrs. Claus: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbd"), "Mrs. Claus: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbe"), "Mrs. Claus: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbf"), "Mrs. Claus: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\x84"), "mx claus"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8"), "superhero"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb"), "superhero: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc"), "superhero: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd"), "superhero: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe"), "superhero: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf"), "superhero: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xe2\x80\x8d\xe2\x99\x82"), "man superhero"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xe2\x80\x8d\xe2\x99\x80"), "woman superhero"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9"), "supervillain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb"), "supervillain: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc"), + "supervillain: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd"), "supervillain: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe"), + "supervillain: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf"), "supervillain: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xe2\x80\x8d\xe2\x99\x82"), "man supervillain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xe2\x80\x8d\xe2\x99\x80"), "woman supervillain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99"), "mage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb"), "mage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc"), "mage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd"), "mage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe"), "mage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf"), "mage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xe2\x80\x8d\xe2\x99\x82"), "man mage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man mage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man mage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man mage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man mage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man mage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xe2\x80\x8d\xe2\x99\x80"), "woman mage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a"), "fairy"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb"), "fairy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc"), "fairy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd"), "fairy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe"), "fairy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf"), "fairy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xe2\x80\x8d\xe2\x99\x82"), "man fairy"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xe2\x80\x8d\xe2\x99\x80"), "woman fairy"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b"), "vampire"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb"), "vampire: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc"), "vampire: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd"), "vampire: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe"), "vampire: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf"), "vampire: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xe2\x80\x8d\xe2\x99\x82"), "man vampire"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xe2\x80\x8d\xe2\x99\x80"), "woman vampire"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c"), "merperson"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb"), "merperson: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc"), "merperson: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd"), "merperson: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe"), "merperson: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf"), "merperson: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xe2\x80\x8d\xe2\x99\x82"), "merman"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "merman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "merman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "merman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "merman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "merman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xe2\x80\x8d\xe2\x99\x80"), "mermaid"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d"), "elf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb"), "elf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc"), "elf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd"), "elf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe"), "elf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf"), "elf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xe2\x80\x8d\xe2\x99\x82"), "man elf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man elf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man elf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man elf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man elf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man elf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xe2\x80\x8d\xe2\x99\x80"), "woman elf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9e"), "genie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9e\xe2\x80\x8d\xe2\x99\x82"), "man genie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9e\xe2\x80\x8d\xe2\x99\x80"), "woman genie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9f"), "zombie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9f\xe2\x80\x8d\xe2\x99\x82"), "man zombie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9f\xe2\x80\x8d\xe2\x99\x80"), "woman zombie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86"), "person getting massage"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb"), + "person getting massage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc"), + "person getting massage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd"), + "person getting massage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe"), + "person getting massage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf"), + "person getting massage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xe2\x80\x8d\xe2\x99\x82"), "man getting massage"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xe2\x80\x8d\xe2\x99\x80"), "woman getting massage"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87"), "person getting haircut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb"), + "person getting haircut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc"), + "person getting haircut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd"), + "person getting haircut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe"), + "person getting haircut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf"), + "person getting haircut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xe2\x80\x8d\xe2\x99\x82"), "man getting haircut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xe2\x80\x8d\xe2\x99\x80"), "woman getting haircut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6"), "person walking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb"), "person walking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc"), + "person walking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd"), "person walking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe"), + "person walking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf"), "person walking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x82"), "man walking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man walking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man walking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man walking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man walking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man walking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x80"), "woman walking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d"), "person standing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb"), "person standing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc"), + "person standing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd"), "person standing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe"), + "person standing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf"), "person standing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xe2\x80\x8d\xe2\x99\x82"), "man standing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man standing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man standing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man standing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man standing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man standing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xe2\x80\x8d\xe2\x99\x80"), "woman standing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e"), "person kneeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb"), "person kneeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc"), + "person kneeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd"), "person kneeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe"), + "person kneeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf"), "person kneeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x82"), "man kneeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x80"), "woman kneeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xaf"), "man with probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83"), "person running"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb"), "person running: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc"), + "person running: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd"), "person running: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe"), + "person running: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf"), "person running: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x82"), "man running"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man running: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man running: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man running: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man running: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man running: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x80"), "woman running"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman running: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman running: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman running: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman running: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman running: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83"), "woman dancing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbb"), "woman dancing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbc"), + "woman dancing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbd"), "woman dancing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbe"), + "woman dancing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbf"), "woman dancing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba"), "man dancing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbb"), "man dancing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbc"), + "man dancing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbd"), "man dancing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbe"), + "man dancing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbf"), "man dancing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4"), "man in suit levitating"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbb"), + "man in suit levitating: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbc"), + "man in suit levitating: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbd"), + "man in suit levitating: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbe"), + "man in suit levitating: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbf"), + "man in suit levitating: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf"), "people with bunny ears"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf\xe2\x80\x8d\xe2\x99\x82"), "men with bunny ears"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf\xe2\x80\x8d\xe2\x99\x80"), "women with bunny ears"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96"), "person in steamy room"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb"), + "person in steamy room: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc"), + "person in steamy room: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd"), + "person in steamy room: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe"), + "person in steamy room: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf"), + "person in steamy room: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xe2\x80\x8d\xe2\x99\x82"), "man in steamy room"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xe2\x80\x8d\xe2\x99\x80"), "woman in steamy room"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97"), "person climbing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb"), "person climbing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc"), + "person climbing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd"), "person climbing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe"), + "person climbing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf"), "person climbing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xe2\x80\x8d\xe2\x99\x82"), "man climbing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xe2\x80\x8d\xe2\x99\x80"), "woman climbing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb7"), "ninja"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xba"), "person fencing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87"), "horse racing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbb"), "horse racing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbc"), + "horse racing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbd"), "horse racing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbe"), + "horse racing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbf"), "horse racing: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb7"), "skier"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82"), "snowboarder"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbb"), "snowboarder: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbc"), + "snowboarder: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbd"), "snowboarder: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbe"), + "snowboarder: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbf"), "snowboarder: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c"), "person golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb"), "person golfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc"), + "person golfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd"), "person golfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe"), + "person golfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf"), "person golfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), "man golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xe2\x80\x8d\xe2\x99\x82"), "man golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), "woman golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xe2\x80\x8d\xe2\x99\x80"), "woman golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84"), "person surfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb"), "person surfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc"), + "person surfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd"), "person surfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe"), + "person surfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf"), "person surfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xe2\x80\x8d\xe2\x99\x82"), "man surfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xe2\x80\x8d\xe2\x99\x80"), "woman surfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3"), "person rowing boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb"), + "person rowing boat: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc"), + "person rowing boat: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd"), + "person rowing boat: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe"), + "person rowing boat: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf"), + "person rowing boat: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xe2\x80\x8d\xe2\x99\x82"), "man rowing boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xe2\x80\x8d\xe2\x99\x80"), "woman rowing boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a"), "person swimming"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb"), "person swimming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc"), + "person swimming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd"), "person swimming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe"), + "person swimming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf"), "person swimming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xe2\x80\x8d\xe2\x99\x82"), "man swimming"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xe2\x80\x8d\xe2\x99\x80"), "woman swimming"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9"), "person bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbb"), "person bouncing ball: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbc"), + "person bouncing ball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbd"), + "person bouncing ball: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbe"), + "person bouncing ball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbf"), "person bouncing ball: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), "man bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xe2\x80\x8d\xe2\x99\x82"), "man bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xe2\x80\x8d\xe2\x99\x80"), "woman bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b"), "person lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb"), + "person lifting weights: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc"), + "person lifting weights: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd"), + "person lifting weights: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe"), + "person lifting weights: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf"), + "person lifting weights: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xe2\x80\x8d\xe2\x99\x82"), "man lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xe2\x80\x8d\xe2\x99\x80"), "woman lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4"), "person biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb"), "person biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc"), + "person biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd"), "person biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe"), + "person biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf"), "person biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xe2\x80\x8d\xe2\x99\x82"), "man biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xe2\x80\x8d\xe2\x99\x80"), "woman biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5"), "person mountain biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb"), + "person mountain biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc"), + "person mountain biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd"), + "person mountain biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe"), + "person mountain biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf"), + "person mountain biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xe2\x80\x8d\xe2\x99\x82"), "man mountain biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xe2\x80\x8d\xe2\x99\x80"), "woman mountain biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8"), "person cartwheeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb"), + "person cartwheeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc"), + "person cartwheeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd"), + "person cartwheeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe"), + "person cartwheeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf"), + "person cartwheeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xe2\x80\x8d\xe2\x99\x82"), "man cartwheeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xe2\x80\x8d\xe2\x99\x80"), "woman cartwheeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc"), "people wrestling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xe2\x80\x8d\xe2\x99\x82"), "men wrestling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xe2\x80\x8d\xe2\x99\x80"), "women wrestling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd"), "person playing water polo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb"), + "person playing water polo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc"), + "person playing water polo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd"), + "person playing water polo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe"), + "person playing water polo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf"), + "person playing water polo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xe2\x80\x8d\xe2\x99\x82"), "man playing water polo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xe2\x80\x8d\xe2\x99\x80"), "woman playing water polo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe"), "person playing handball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb"), + "person playing handball: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc"), + "person playing handball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd"), + "person playing handball: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe"), + "person playing handball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf"), + "person playing handball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xe2\x80\x8d\xe2\x99\x82"), "man playing handball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xe2\x80\x8d\xe2\x99\x80"), "woman playing handball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9"), "person juggling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb"), "person juggling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc"), + "person juggling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd"), "person juggling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe"), + "person juggling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf"), "person juggling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xe2\x80\x8d\xe2\x99\x82"), "man juggling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xe2\x80\x8d\xe2\x99\x80"), "woman juggling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98"), "person in lotus position"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb"), + "person in lotus position: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc"), + "person in lotus position: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd"), + "person in lotus position: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe"), + "person in lotus position: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf"), + "person in lotus position: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xe2\x80\x8d\xe2\x99\x82"), "man in lotus position"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xe2\x80\x8d\xe2\x99\x80"), "woman in lotus position"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80"), "person taking bath"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbb"), + "person taking bath: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbc"), + "person taking bath: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbd"), + "person taking bath: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbe"), + "person taking bath: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbf"), + "person taking bath: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c"), "person in bed"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbb"), "person in bed: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbc"), + "person in bed: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbd"), "person in bed: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbe"), + "person in bed: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbf"), "person in bed: dark skin tone"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91"), + "people holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad"), "women holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbb"), + "women holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbc"), + "women holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbd"), + "women holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbe"), + "women holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbf"), + "women holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab"), "woman and man holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbb"), + "woman and man holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbc"), + "woman and man holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbd"), + "woman and man holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbe"), + "woman and man holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbf"), + "woman and man holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac"), "men holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbb"), + "men holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbc"), + "men holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbd"), + "men holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbe"), + "men holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbf"), "men holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8f"), "kiss"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f" + "\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: woman, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2" + "\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: woman, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f" + "\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: man, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2" + "\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: man, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f" + "\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9"), + "kiss: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2" + "\x80\x8d\xf0\x9f\x91\xa9"), + "kiss: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x91"), "couple with heart"}, + Emoji{QString::fromUtf8( + "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: woman, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: woman, man"}, + Emoji{QString::fromUtf8( + "\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: man, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: man, man"}, + Emoji{QString::fromUtf8( + "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9"), + "couple with heart: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x91\xa9"), + "couple with heart: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaa"), "family"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, woman, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, woman, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, woman, girl, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, woman, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, woman, girl, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, man, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, man, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, man, girl, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, man, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, man, girl, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, woman, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: woman, woman, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, woman, girl, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, woman, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: woman, woman, girl, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6"), "family: man, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7"), "family: man, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, girl, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, girl, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6"), "family: woman, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"), "family: woman, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, girl, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: woman, girl, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xa3"), "speaking head"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa4"), "bust in silhouette"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa5"), "busts in silhouette"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x82"), "people hugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa3"), "footprints"}, }; -const std::vector<Emoji> Provider::nature = { - Emoji{QString::fromUtf8("\xf0\x9f\x99\x88"), ":see_no_evil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x89"), ":hear_no_evil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8a"), ":speak_no_evil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa6"), ":sweat_drops:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa8"), ":dash:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb5"), ":monkey_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x92"), ":monkey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8d"), ":gorilla:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb6"), ":dog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x95"), ":dog2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa9"), ":poodle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xba"), ":wolf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8a"), ":fox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb1"), ":cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x88"), ":cat2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x81"), ":lion_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xaf"), ":tiger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x85"), ":tiger2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x86"), ":leopard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb4"), ":horse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8e"), ":racehorse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8c"), ":deer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x84"), ":unicorn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xae"), ":cow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x82"), ":ox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x83"), ":water_buffalo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x84"), ":cow2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb7"), ":pig:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x96"), ":pig2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x97"), ":boar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbd"), ":pig_nose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8f"), ":ram:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x91"), ":sheep:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x90"), ":goat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xaa"), ":dromedary_camel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xab"), ":camel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x98"), ":elephant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8f"), ":rhino:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xad"), ":mouse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x81"), ":mouse2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x80"), ":rat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb9"), ":hamster:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb0"), ":rabbit:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x87"), ":rabbit2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbf"), ":chipmunk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x87"), ":bat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbb"), ":bear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa8"), ":koala:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbc"), ":panda_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbe"), ":feet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x83"), ":turkey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x94"), ":chicken:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x93"), ":rooster:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa3"), ":hatching_chick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa4"), ":baby_chick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa5"), ":hatched_chick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa6"), ":bird:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa7"), ":penguin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8a"), ":dove:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x85"), ":eagle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x86"), ":duck:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x89"), ":owl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb8"), ":frog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8a"), ":crocodile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa2"), ":turtle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8e"), ":lizard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8d"), ":snake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb2"), ":dragon_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x89"), ":dragon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb3"), ":whale:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8b"), ":whale2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xac"), ":dolphin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9f"), ":fish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa0"), ":tropical_fish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa1"), ":blowfish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x88"), ":shark:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x99"), ":octopus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9a"), ":shell:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x80"), ":crab:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x90"), ":shrimp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x91"), ":squid:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8b"), ":butterfly:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8c"), ":snail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9b"), ":bug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9c"), ":ant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9d"), ":bee:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9e"), ":beetle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb7"), ":spider:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb8"), ":spider_web:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x82"), ":scorpion:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x90"), ":bouquet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb8"), ":cherry_blossom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb5"), ":rosette:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb9"), ":rose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x80"), ":wilted_rose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xba"), ":hibiscus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbb"), ":sunflower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbc"), ":blossom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb7"), ":tulip:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb1"), ":seedling:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb2"), ":evergreen_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb3"), ":deciduous_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb4"), ":palm_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb5"), ":cactus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbe"), ":ear_of_rice:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbf"), ":herb:"}, - Emoji{QString::fromUtf8("\xe2\x98\x98"), ":shamrock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x80"), ":four_leaf_clover:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x81"), ":maple_leaf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x82"), ":fallen_leaf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x83"), ":leaves:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x84"), ":mushroom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb0"), ":chestnut:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8d"), ":earth_africa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8e"), ":earth_americas:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8f"), ":earth_asia:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x91"), ":new_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x92"), ":waxing_crescent_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x93"), ":first_quarter_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x94"), ":waxing_gibbous_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x95"), ":full_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x96"), ":waning_gibbous_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x97"), ":last_quarter_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x98"), ":waning_crescent_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x99"), ":crescent_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9a"), ":new_moon_with_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9b"), ":first_quarter_moon_with_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9c"), ":last_quarter_moon_with_face:"}, - Emoji{QString::fromUtf8("\xe2\x98\x80"), ":sunny:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9d"), ":full_moon_with_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9e"), ":sun_with_face:"}, - Emoji{QString::fromUtf8("\xe2\xad\x90"), ":star:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9f"), ":star2:"}, - Emoji{QString::fromUtf8("\xe2\x98\x81"), ":cloud:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x85"), ":partly_sunny:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x88"), ":thunder_cloud_rain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa4"), ":white_sun_small_cloud:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa5"), ":white_sun_cloud:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa6"), ":white_sun_rain_cloud:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa7"), ":cloud_rain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa8"), ":cloud_snow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa9"), ":cloud_lightning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaa"), ":cloud_tornado:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xab"), ":fog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xac"), ":wind_blowing_face:"}, - Emoji{QString::fromUtf8("\xe2\x98\x82"), ":umbrella2:"}, - Emoji{QString::fromUtf8("\xe2\x98\x94"), ":umbrella:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xa1"), ":zap:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x84"), ":snowflake:"}, - Emoji{QString::fromUtf8("\xe2\x98\x83"), ":snowman2:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x84"), ":snowman:"}, - Emoji{QString::fromUtf8("\xe2\x98\x84"), ":comet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa5"), ":fire:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa7"), ":droplet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8a"), ":ocean:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x83"), ":jack_o_lantern:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x84"), ":christmas_tree:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xa8"), ":sparkles:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8b"), ":tanabata_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8d"), ":bamboo:"}, +const std::vector<Emoji> emoji::Provider::nature = { + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb5"), "monkey face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x92"), "monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8d"), "gorilla"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa7"), "orangutan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb6"), "dog face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x95"), "dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xae"), "guide dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x95\xe2\x80\x8d\xf0\x9f\xa6\xba"), "service dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa9"), "poodle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xba"), "wolf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8a"), "fox"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9d"), "raccoon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb1"), "cat face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x88"), "cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x88\xe2\x80\x8d\xe2\xac\x9b"), "black cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x81"), "lion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xaf"), "tiger face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x85"), "tiger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x86"), "leopard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb4"), "horse face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8e"), "horse"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x84"), "unicorn"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x93"), "zebra"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8c"), "deer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xac"), "bison"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xae"), "cow face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x82"), "ox"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x83"), "water buffalo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x84"), "cow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb7"), "pig face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x96"), "pig"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x97"), "boar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbd"), "pig nose"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8f"), "ram"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x91"), "ewe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x90"), "goat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xaa"), "camel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xab"), "two-hump camel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x99"), "llama"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x92"), "giraffe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x98"), "elephant"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa3"), "mammoth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8f"), "rhinoceros"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9b"), "hippopotamus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xad"), "mouse face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x81"), "mouse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x80"), "rat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb9"), "hamster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb0"), "rabbit face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x87"), "rabbit"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbf"), "chipmunk"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xab"), "beaver"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x94"), "hedgehog"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x87"), "bat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbb"), "bear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbb\xe2\x80\x8d\xe2\x9d\x84"), "polar bear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa8"), "koala"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbc"), "panda"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa5"), "sloth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa6"), "otter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa8"), "skunk"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x98"), "kangaroo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa1"), "badger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbe"), "paw prints"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x83"), "turkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x94"), "chicken"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x93"), "rooster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa3"), "hatching chick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa4"), "baby chick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa5"), "front-facing baby chick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa6"), "bird"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa7"), "penguin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8a"), "dove"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x85"), "eagle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x86"), "duck"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa2"), "swan"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x89"), "owl"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa4"), "dodo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb6"), "feather"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa9"), "flamingo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9a"), "peacock"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9c"), "parrot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb8"), "frog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8a"), "crocodile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa2"), "turtle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8e"), "lizard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8d"), "snake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb2"), "dragon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x89"), "dragon"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x95"), "sauropod"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x96"), "T-Rex"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb3"), "spouting whale"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8b"), "whale"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xac"), "dolphin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xad"), "seal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9f"), "fish"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa0"), "tropical fish"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa1"), "blowfish"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x88"), "shark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x99"), "octopus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9a"), "spiral shell"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8c"), "snail"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8b"), "butterfly"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9b"), "bug"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9c"), "ant"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9d"), "honeybee"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb2"), "beetle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9e"), "lady beetle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x97"), "cricket"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb3"), "cockroach"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb7"), "spider"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb8"), "spider web"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x82"), "scorpion"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9f"), "mosquito"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb0"), "fly"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb1"), "worm"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa0"), "microbe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x90"), "bouquet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb8"), "cherry blossom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xae"), "white flower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb5"), "rosette"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb9"), "rose"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x80"), "wilted flower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xba"), "hibiscus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbb"), "sunflower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbc"), "blossom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb7"), "tulip"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb1"), "seedling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb4"), "potted plant"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb2"), "evergreen tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb3"), "deciduous tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb4"), "palm tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb5"), "cactus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbe"), "sheaf of rice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbf"), "herb"}, + Emoji{QString::fromUtf8("\xe2\x98\x98"), "shamrock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x80"), "four leaf clover"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x81"), "maple leaf"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x82"), "fallen leaf"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x83"), "leaf fluttering in wind"}, }; -const std::vector<Emoji> Provider::food = { - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x87"), ":grapes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x88"), ":melon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x89"), ":watermelon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8a"), ":tangerine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8b"), ":lemon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8c"), ":banana:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8d"), ":pineapple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8e"), ":apple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8f"), ":green_apple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x90"), ":pear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x91"), ":peach:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x92"), ":cherries:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x93"), ":strawberry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9d"), ":kiwi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x85"), ":tomato:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x91"), ":avocado:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x86"), ":eggplant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x94"), ":potato:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x95"), ":carrot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbd"), ":corn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb6"), ":hot_pepper:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x92"), ":cucumber:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9c"), ":peanuts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9e"), ":bread:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x90"), ":croissant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x96"), ":french_bread:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9e"), ":pancakes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa7\x80"), ":cheese:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x96"), ":meat_on_bone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x97"), ":poultry_leg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x93"), ":bacon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x94"), ":hamburger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9f"), ":fries:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x95"), ":pizza:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xad"), ":hotdog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xae"), ":taco:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaf"), ":burrito:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x99"), ":stuffed_flatbread:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9a"), ":egg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb3"), ":cooking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x98"), ":shallow_pan_of_food:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb2"), ":stew:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x97"), ":salad:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbf"), ":popcorn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb1"), ":bento:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x98"), ":rice_cracker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x99"), ":rice_ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9a"), ":rice:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9b"), ":curry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9c"), ":ramen:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9d"), ":spaghetti:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa0"), ":sweet_potato:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa2"), ":oden:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa3"), ":sushi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa4"), ":fried_shrimp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa5"), ":fish_cake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa1"), ":dango:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa6"), ":icecream:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa7"), ":shaved_ice:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa8"), ":ice_cream:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa9"), ":doughnut:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaa"), ":cookie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x82"), ":birthday:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb0"), ":cake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xab"), ":chocolate_bar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xac"), ":candy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xad"), ":lollipop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xae"), ":custard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaf"), ":honey_pot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbc"), ":baby_bottle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9b"), ":milk:"}, - Emoji{QString::fromUtf8("\xe2\x98\x95"), ":coffee:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb5"), ":tea:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb6"), ":sake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbe"), ":champagne:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb7"), ":wine_glass:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb8"), ":cocktail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb9"), ":tropical_drink:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xba"), ":beer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbb"), ":beers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x82"), ":champagne_glass:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x83"), ":tumbler_glass:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbd"), ":fork_knife_plate:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb4"), ":fork_and_knife:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x84"), ":spoon:"}, +const std::vector<Emoji> emoji::Provider::food = { + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x87"), "grapes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x88"), "melon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x89"), "watermelon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8a"), "tangerine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8b"), "lemon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8c"), "banana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8d"), "pineapple"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xad"), "mango"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8e"), "red apple"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8f"), "green apple"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x90"), "pear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x91"), "peach"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x92"), "cherries"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x93"), "strawberry"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x90"), "blueberries"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9d"), "kiwi fruit"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x85"), "tomato"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x92"), "olive"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa5"), "coconut"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x91"), "avocado"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x86"), "eggplant"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x94"), "potato"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x95"), "carrot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbd"), "ear of corn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb6"), "hot pepper"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x91"), "bell pepper"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x92"), "cucumber"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xac"), "leafy green"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa6"), "broccoli"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x84"), "garlic"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x85"), "onion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x84"), "mushroom"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9c"), "peanuts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb0"), "chestnut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9e"), "bread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x90"), "croissant"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x96"), "baguette bread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x93"), "flatbread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa8"), "pretzel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xaf"), "bagel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9e"), "pancakes"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x87"), "waffle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x80"), "cheese wedge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x96"), "meat on bone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x97"), "poultry leg"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa9"), "cut of meat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x93"), "bacon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x94"), "hamburger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9f"), "french fries"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x95"), "pizza"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xad"), "hot dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xaa"), "sandwich"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xae"), "taco"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaf"), "burrito"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x94"), "tamale"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x99"), "stuffed flatbread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x86"), "falafel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9a"), "egg"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb3"), "cooking"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x98"), "shallow pan of food"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb2"), "pot of food"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x95"), "fondue"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa3"), "bowl with spoon"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x97"), "green salad"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbf"), "popcorn"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x88"), "butter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x82"), "salt"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xab"), "canned food"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb1"), "bento box"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x98"), "rice cracker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x99"), "rice ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9a"), "cooked rice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9b"), "curry rice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9c"), "steaming bowl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9d"), "spaghetti"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa0"), "roasted sweet potato"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa2"), "oden"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa3"), "sushi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa4"), "fried shrimp"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa5"), "fish cake with swirl"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xae"), "moon cake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa1"), "dango"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9f"), "dumpling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa0"), "fortune cookie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa1"), "takeout box"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x80"), "crab"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9e"), "lobster"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x90"), "shrimp"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x91"), "squid"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xaa"), "oyster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa6"), "soft ice cream"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa7"), "shaved ice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa8"), "ice cream"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa9"), "doughnut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaa"), "cookie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x82"), "birthday cake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb0"), "shortcake"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x81"), "cupcake"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa7"), "pie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xab"), "chocolate bar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xac"), "candy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xad"), "lollipop"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xae"), "custard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaf"), "honey pot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbc"), "baby bottle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9b"), "glass of milk"}, + Emoji{QString::fromUtf8("\xe2\x98\x95"), "hot beverage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x96"), "teapot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb5"), "teacup without handle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb6"), "sake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbe"), "bottle with popping cork"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb7"), "wine glass"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb8"), "cocktail glass"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb9"), "tropical drink"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xba"), "beer mug"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbb"), "clinking beer mugs"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x82"), "clinking glasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x83"), "tumbler glass"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa4"), "cup with straw"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8b"), "bubble tea"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x83"), "beverage box"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x89"), "mate"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8a"), "ice"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa2"), "chopsticks"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbd"), "fork and knife with plate"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb4"), "fork and knife"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x84"), "spoon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xaa"), "kitchen knife"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xba"), "amphora"}, }; -const std::vector<Emoji> Provider::activity = { - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbe"), ":space_invader:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4"), ":levitate:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xba"), ":fencer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87"), ":horse_racing:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb7"), ":skier:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82"), ":snowboarder:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c"), ":golfer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84"), ":surfer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3"), ":rowboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a"), ":swimmer:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb9"), ":basketball_player:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b"), ":lifter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4"), ":bicyclist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5"), ":mountain_bicyclist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8"), ":cartwheel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc"), ":wrestlers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd"), ":water_polo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe"), ":handball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9"), ":juggling:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaa"), ":circus_tent:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xad"), ":performing_arts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa8"), ":art:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb0"), ":slot_machine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80"), ":bath:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x97"), ":reminder_ribbon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9f"), ":tickets:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xab"), ":ticket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x96"), ":military_medal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x86"), ":trophy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x85"), ":medal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x87"), ":first_place:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x88"), ":second_place:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x89"), ":third_place:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xbd"), ":soccer:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xbe"), ":baseball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x80"), ":basketball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x90"), ":volleyball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x88"), ":football:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x89"), ":rugby_football:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbe"), ":tennis:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb1"), ":8ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb3"), ":bowling:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8f"), ":cricket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x91"), ":field_hockey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x92"), ":hockey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x93"), ":ping_pong:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb8"), ":badminton:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8a"), ":boxing_glove:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8b"), ":martial_arts_uniform:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x85"), ":goal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaf"), ":dart:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb3"), ":golf:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb8"), ":ice_skate:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa3"), ":fishing_pole_and_fish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbd"), ":running_shirt_with_sash:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbf"), ":ski:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xae"), ":video_game:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb2"), ":game_die:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbc"), ":musical_score:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa4"), ":microphone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa7"), ":headphones:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb7"), ":saxophone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb8"), ":guitar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb9"), ":musical_keyboard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xba"), ":trumpet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbb"), ":violin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x81"), ":drum:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xac"), ":clapper:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb9"), ":bow_and_arrow:"}, +const std::vector<Emoji> emoji::Provider::activity = { + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x83"), "jack-o-lantern"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x84"), "Christmas tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x86"), "fireworks"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x87"), "sparkler"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa8"), "firecracker"}, + Emoji{QString::fromUtf8("\xe2\x9c\xa8"), "sparkles"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x88"), "balloon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x89"), "party popper"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8a"), "confetti ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8b"), "tanabata tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8d"), "pine decoration"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8e"), "Japanese dolls"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8f"), "carp streamer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x90"), "wind chime"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x91"), "moon viewing ceremony"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa7"), "red envelope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x80"), "ribbon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x81"), "wrapped gift"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x97"), "reminder ribbon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9f"), "admission tickets"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xab"), "ticket"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x96"), "military medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x86"), "trophy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x85"), "sports medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x87"), "1st place medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x88"), "2nd place medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x89"), "3rd place medal"}, + Emoji{QString::fromUtf8("\xe2\x9a\xbd"), "soccer ball"}, + Emoji{QString::fromUtf8("\xe2\x9a\xbe"), "baseball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8e"), "softball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x80"), "basketball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x90"), "volleyball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x88"), "american football"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x89"), "rugby football"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbe"), "tennis"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8f"), "flying disc"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb3"), "bowling"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8f"), "cricket game"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x91"), "field hockey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x92"), "ice hockey"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8d"), "lacrosse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x93"), "ping pong"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb8"), "badminton"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8a"), "boxing glove"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8b"), "martial arts uniform"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x85"), "goal net"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb3"), "flag in hole"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb8"), "ice skate"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa3"), "fishing pole"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbf"), "diving mask"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbd"), "running shirt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbf"), "skis"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb7"), "sled"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8c"), "curling stone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaf"), "direct hit"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x80"), "yo-yo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x81"), "kite"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb1"), "pool 8 ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xae"), "crystal ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x84"), "magic wand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbf"), "nazar amulet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xae"), "video game"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb9"), "joystick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb0"), "slot machine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb2"), "game die"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa9"), "puzzle piece"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb8"), "teddy bear"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x85"), "piñata"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x86"), "nesting dolls"}, + Emoji{QString::fromUtf8("\xe2\x99\xa0"), "spade suit"}, + Emoji{QString::fromUtf8("\xe2\x99\xa5"), "heart suit"}, + Emoji{QString::fromUtf8("\xe2\x99\xa6"), "diamond suit"}, + Emoji{QString::fromUtf8("\xe2\x99\xa3"), "club suit"}, + Emoji{QString::fromUtf8("\xe2\x99\x9f"), "chess pawn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x83\x8f"), "joker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x80\x84"), "mahjong red dragon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb4"), "flower playing cards"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xad"), "performing arts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xbc"), "framed picture"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa8"), "artist palette"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb5"), "thread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa1"), "sewing needle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb6"), "yarn"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa2"), "knot"}, }; -const std::vector<Emoji> Provider::travel = { - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8e"), ":race_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8d"), ":motorcycle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbe"), ":japan:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x94"), ":mountain_snow:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb0"), ":mountain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8b"), ":volcano:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbb"), ":mount_fuji:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x95"), ":camping:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x96"), ":beach:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9c"), ":desert:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9d"), ":island:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9e"), ":park:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9f"), ":stadium:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9b"), ":classical_building:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x97"), ":construction_site:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x98"), ":homes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x99"), ":cityscape:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9a"), ":house_abandoned:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa0"), ":house:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa1"), ":house_with_garden:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa2"), ":office:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa3"), ":post_office:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa4"), ":european_post_office:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa5"), ":hospital:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa6"), ":bank:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa8"), ":hotel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa9"), ":love_hotel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaa"), ":convenience_store:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xab"), ":school:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xac"), ":department_store:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xad"), ":factory:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaf"), ":japanese_castle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb0"), ":european_castle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x92"), ":wedding:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbc"), ":tokyo_tower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbd"), ":statue_of_liberty:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xaa"), ":church:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8c"), ":mosque:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8d"), ":synagogue:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xa9"), ":shinto_shrine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8b"), ":kaaba:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb2"), ":fountain:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xba"), ":tent:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x81"), ":foggy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x83"), ":night_with_stars:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x84"), ":sunrise_over_mountains:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x85"), ":sunrise:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x86"), ":city_dusk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x87"), ":city_sunset:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x89"), ":bridge_at_night:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8c"), ":milky_way:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa0"), ":carousel_horse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa1"), ":ferris_wheel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa2"), ":roller_coaster:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x82"), ":steam_locomotive:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x83"), ":railway_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x84"), ":bullettrain_side:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x85"), ":bullettrain_front:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x86"), ":train2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x87"), ":metro:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x88"), ":light_rail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x89"), ":station:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8a"), ":tram:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9d"), ":monorail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9e"), ":mountain_railway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8b"), ":train:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8c"), ":bus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8d"), ":oncoming_bus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8e"), ":trolleybus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x90"), ":minibus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x91"), ":ambulance:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x92"), ":fire_engine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x93"), ":police_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x94"), ":oncoming_police_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x95"), ":taxi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x96"), ":oncoming_taxi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x97"), ":red_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x98"), ":oncoming_automobile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x99"), ":blue_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9a"), ":truck:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9b"), ":articulated_lorry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9c"), ":tractor:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb2"), ":bike:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb4"), ":scooter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb5"), ":motor_scooter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8f"), ":busstop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa3"), ":motorway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa4"), ":railway_track:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xbd"), ":fuelpump:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa8"), ":rotating_light:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa5"), ":traffic_light:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa6"), ":vertical_traffic_light:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa7"), ":construction:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x93"), ":anchor:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb5"), ":sailboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb6"), ":canoe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa4"), ":speedboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb3"), ":cruise_ship:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb4"), ":ferry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa5"), ":motorboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa2"), ":ship:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x88"), ":airplane:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa9"), ":airplane_small:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xab"), ":airplane_departure:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xac"), ":airplane_arriving:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xba"), ":seat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x81"), ":helicopter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9f"), ":suspension_railway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa0"), ":mountain_cableway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa1"), ":aerial_tramway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x80"), ":rocket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb0"), ":satellite_orbital:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa0"), ":stars:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x88"), ":rainbow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x86"), ":fireworks:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x87"), ":sparkler:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x91"), ":rice_scene:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x81"), ":checkered_flag:"}, +const std::vector<Emoji> emoji::Provider::travel = { + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8d"), "globe showing Europe-Africa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8e"), "globe showing Americas"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8f"), "globe showing Asia-Australia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x90"), "globe with meridians"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xba"), "world map"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbe"), "map of Japan"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xad"), "compass"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x94"), "snow-capped mountain"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb0"), "mountain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8b"), "volcano"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbb"), "mount fuji"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x95"), "camping"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x96"), "beach with umbrella"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9c"), "desert"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9d"), "desert island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9e"), "national park"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9f"), "stadium"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9b"), "classical building"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x97"), "building construction"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb1"), "brick"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa8"), "rock"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb5"), "wood"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x96"), "hut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x98"), "houses"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9a"), "derelict house"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa0"), "house"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa1"), "house with garden"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa2"), "office building"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa3"), "Japanese post office"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa4"), "post office"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa5"), "hospital"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa6"), "bank"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa8"), "hotel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa9"), "love hotel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaa"), "convenience store"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xab"), "school"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xac"), "department store"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xad"), "factory"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaf"), "Japanese castle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb0"), "castle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x92"), "wedding"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbc"), "Tokyo tower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbd"), "Statue of Liberty"}, + Emoji{QString::fromUtf8("\xe2\x9b\xaa"), "church"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8c"), "mosque"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x95"), "hindu temple"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8d"), "synagogue"}, + Emoji{QString::fromUtf8("\xe2\x9b\xa9"), "shinto shrine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8b"), "kaaba"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb2"), "fountain"}, + Emoji{QString::fromUtf8("\xe2\x9b\xba"), "tent"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x81"), "foggy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x83"), "night with stars"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x99"), "cityscape"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x84"), "sunrise over mountains"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x85"), "sunrise"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x86"), "cityscape at dusk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x87"), "sunset"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x89"), "bridge at night"}, + Emoji{QString::fromUtf8("\xe2\x99\xa8"), "hot springs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa0"), "carousel horse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa1"), "ferris wheel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa2"), "roller coaster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x88"), "barber pole"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaa"), "circus tent"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x82"), "locomotive"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x83"), "railway car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x84"), "high-speed train"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x85"), "bullet train"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x86"), "train"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x87"), "metro"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x88"), "light rail"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x89"), "station"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8a"), "tram"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9d"), "monorail"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9e"), "mountain railway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8b"), "tram car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8c"), "bus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8d"), "oncoming bus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8e"), "trolleybus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x90"), "minibus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x91"), "ambulance"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x92"), "fire engine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x93"), "police car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x94"), "oncoming police car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x95"), "taxi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x96"), "oncoming taxi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x97"), "automobile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x98"), "oncoming automobile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x99"), "sport utility vehicle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xbb"), "pickup truck"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9a"), "delivery truck"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9b"), "articulated lorry"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9c"), "tractor"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8e"), "racing car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8d"), "motorcycle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb5"), "motor scooter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbd"), "manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbc"), "motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xba"), "auto rickshaw"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb2"), "bicycle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb4"), "kick scooter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb9"), "skateboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xbc"), "roller skate"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8f"), "bus stop"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa3"), "motorway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa4"), "railway track"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa2"), "oil drum"}, + Emoji{QString::fromUtf8("\xe2\x9b\xbd"), "fuel pump"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa8"), "police car light"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa5"), "horizontal traffic light"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa6"), "vertical traffic light"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x91"), "stop sign"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa7"), "construction"}, + Emoji{QString::fromUtf8("\xe2\x9a\x93"), "anchor"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb5"), "sailboat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb6"), "canoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa4"), "speedboat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb3"), "passenger ship"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb4"), "ferry"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa5"), "motor boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa2"), "ship"}, + Emoji{QString::fromUtf8("\xe2\x9c\x88"), "airplane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa9"), "small airplane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xab"), "airplane departure"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xac"), "airplane arrival"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x82"), "parachute"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xba"), "seat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x81"), "helicopter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9f"), "suspension railway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa0"), "mountain cableway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa1"), "aerial tramway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb0"), "satellite"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x80"), "rocket"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb8"), "flying saucer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8e"), "bellhop bell"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb3"), "luggage"}, + Emoji{QString::fromUtf8("\xe2\x8c\x9b"), "hourglass done"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb3"), "hourglass not done"}, + Emoji{QString::fromUtf8("\xe2\x8c\x9a"), "watch"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb0"), "alarm clock"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb1"), "stopwatch"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb2"), "timer clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb0"), "mantelpiece clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9b"), "twelve o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa7"), "twelve-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x90"), "one o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9c"), "one-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x91"), "two o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9d"), "two-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x92"), "three o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9e"), "three-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x93"), "four o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9f"), "four-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x94"), "five o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa0"), "five-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x95"), "six o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa1"), "six-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x96"), "seven o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa2"), "seven-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x97"), "eight o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa3"), "eight-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x98"), "nine o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa4"), "nine-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x99"), "ten o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa5"), "ten-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9a"), "eleven o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa6"), "eleven-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x91"), "new moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x92"), "waxing crescent moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x93"), "first quarter moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x94"), "waxing gibbous moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x95"), "full moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x96"), "waning gibbous moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x97"), "last quarter moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x98"), "waning crescent moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x99"), "crescent moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9a"), "new moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9b"), "first quarter moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9c"), "last quarter moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa1"), "thermometer"}, + Emoji{QString::fromUtf8("\xe2\x98\x80"), "sun"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9d"), "full moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9e"), "sun with face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x90"), "ringed planet"}, + Emoji{QString::fromUtf8("\xe2\xad\x90"), "star"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9f"), "glowing star"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa0"), "shooting star"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8c"), "milky way"}, + Emoji{QString::fromUtf8("\xe2\x98\x81"), "cloud"}, + Emoji{QString::fromUtf8("\xe2\x9b\x85"), "sun behind cloud"}, + Emoji{QString::fromUtf8("\xe2\x9b\x88"), "cloud with lightning and rain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa4"), "sun behind small cloud"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa5"), "sun behind large cloud"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa6"), "sun behind rain cloud"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa7"), "cloud with rain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa8"), "cloud with snow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa9"), "cloud with lightning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaa"), "tornado"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xab"), "fog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xac"), "wind face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x80"), "cyclone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x88"), "rainbow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x82"), "closed umbrella"}, + Emoji{QString::fromUtf8("\xe2\x98\x82"), "umbrella"}, + Emoji{QString::fromUtf8("\xe2\x98\x94"), "umbrella with rain drops"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb1"), "umbrella on ground"}, + Emoji{QString::fromUtf8("\xe2\x9a\xa1"), "high voltage"}, + Emoji{QString::fromUtf8("\xe2\x9d\x84"), "snowflake"}, + Emoji{QString::fromUtf8("\xe2\x98\x83"), "snowman"}, + Emoji{QString::fromUtf8("\xe2\x9b\x84"), "snowman without snow"}, + Emoji{QString::fromUtf8("\xe2\x98\x84"), "comet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa5"), "fire"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa7"), "droplet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8a"), "water wave"}, }; -const std::vector<Emoji> Provider::objects = { - Emoji{QString::fromUtf8("\xe2\x98\xa0"), ":skull_crossbones:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8c"), ":love_letter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa3"), ":bomb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb3"), ":hole:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8d"), ":shopping_bags:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbf"), ":prayer_beads:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8e"), ":gem:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xaa"), ":knife:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xba"), ":amphora:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xba"), ":map:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x88"), ":barber:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xbc"), ":frame_photo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8e"), ":bellhop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaa"), ":door:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c"), ":sleeping_accommodation:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8f"), ":bed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8b"), ":couch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbd"), ":toilet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbf"), ":shower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x81"), ":bathtub:"}, - Emoji{QString::fromUtf8("\xe2\x8c\x9b"), ":hourglass:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb3"), ":hourglass_flowing_sand:"}, - Emoji{QString::fromUtf8("\xe2\x8c\x9a"), ":watch:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb0"), ":alarm_clock:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb1"), ":stopwatch:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb2"), ":timer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb0"), ":clock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa1"), ":thermometer:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb1"), ":beach_umbrella:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x88"), ":balloon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x89"), ":tada:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8a"), ":confetti_ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8e"), ":dolls:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8f"), ":flags:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x90"), ":wind_chime:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x80"), ":ribbon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x81"), ":gift:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb9"), ":joystick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xaf"), ":postal_horn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x99"), ":microphone2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9a"), ":level_slider:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9b"), ":control_knobs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbb"), ":radio:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb1"), ":iphone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb2"), ":calling:"}, - Emoji{QString::fromUtf8("\xe2\x98\x8e"), ":telephone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9e"), ":telephone_receiver:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9f"), ":pager:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa0"), ":fax:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8b"), ":battery:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8c"), ":electric_plug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbb"), ":computer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xa5"), ":desktop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xa8"), ":printer:"}, - Emoji{QString::fromUtf8("\xe2\x8c\xa8"), ":keyboard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xb1"), ":mouse_three_button:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xb2"), ":trackball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbd"), ":minidisc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbe"), ":floppy_disk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbf"), ":cd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x80"), ":dvd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa5"), ":movie_camera:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9e"), ":film_frames:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbd"), ":projector:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xba"), ":tv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb7"), ":camera:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb8"), ":camera_with_flash:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb9"), ":video_camera:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbc"), ":vhs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8d"), ":mag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8e"), ":mag_right:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xac"), ":microscope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xad"), ":telescope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa1"), ":satellite:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xaf"), ":candle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa1"), ":bulb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa6"), ":flashlight:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xae"), ":izakaya_lantern:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x94"), ":notebook_with_decorative_cover:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x95"), ":closed_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x96"), ":book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x97"), ":green_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x98"), ":blue_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x99"), ":orange_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9a"), ":books:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x93"), ":notebook:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x92"), ":ledger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x83"), ":page_with_curl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9c"), ":scroll:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x84"), ":page_facing_up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb0"), ":newspaper:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x9e"), ":newspaper2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x91"), ":bookmark_tabs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x96"), ":bookmark:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb7"), ":label:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb0"), ":moneybag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb4"), ":yen:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb5"), ":dollar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb6"), ":euro:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb7"), ":pound:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb8"), ":money_with_wings:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb3"), ":credit_card:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x89"), ":envelope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa7"), ":e-mail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa8"), ":incoming_envelope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa9"), ":envelope_with_arrow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa4"), ":outbox_tray:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa5"), ":inbox_tray:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa6"), ":package:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xab"), ":mailbox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xaa"), ":mailbox_closed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xac"), ":mailbox_with_mail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xad"), ":mailbox_with_no_mail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xae"), ":postbox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xb3"), ":ballot_box:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8f"), ":pencil2:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x92"), ":black_nib:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8b"), ":pen_fountain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8a"), ":pen_ballpoint:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8c"), ":paintbrush:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8d"), ":crayon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9d"), ":pencil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x81"), ":file_folder:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x82"), ":open_file_folder:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x82"), ":dividers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x85"), ":date:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x86"), ":calendar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x92"), ":notepad_spiral:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x93"), ":calendar_spiral:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x87"), ":card_index:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x88"), ":chart_with_upwards_trend:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x89"), ":chart_with_downwards_trend:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8a"), ":bar_chart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8b"), ":clipboard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8c"), ":pushpin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8d"), ":round_pushpin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8e"), ":paperclip:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x87"), ":paperclips:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8f"), ":straight_ruler:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x90"), ":triangular_ruler:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x82"), ":scissors:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x83"), ":card_box:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x84"), ":file_cabinet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x91"), ":wastebasket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x92"), ":lock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x93"), ":unlock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8f"), ":lock_with_ink_pen:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x90"), ":closed_lock_with_key:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x91"), ":key:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x9d"), ":key2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa8"), ":hammer:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x8f"), ":pick:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x92"), ":hammer_pick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa0"), ":tools:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xa1"), ":dagger:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x94"), ":crossed_swords:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xab"), ":gun:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa1"), ":shield:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa7"), ":wrench:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa9"), ":nut_and_bolt:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x99"), ":gear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x9c"), ":compression:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x97"), ":alembic:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x96"), ":scales:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x97"), ":link:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x93"), ":chains:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x89"), ":syringe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8a"), ":pill:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xac"), ":smoking:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xb0"), ":coffin:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xb1"), ":urn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbf"), ":moyai:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa2"), ":oil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xae"), ":crystal_ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x92"), ":shopping_cart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa9"), ":triangular_flag_on_post:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8c"), ":crossed_flags:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4"), ":flag_black:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3"), ":flag_white:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xf0\x9f\x8c\x88"), ":rainbow_flag:"}, +const std::vector<Emoji> emoji::Provider::objects = { + Emoji{QString::fromUtf8("\xf0\x9f\x91\x93"), "glasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb6"), "sunglasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbd"), "goggles"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbc"), "lab coat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xba"), "safety vest"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x94"), "necktie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x95"), "t-shirt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x96"), "jeans"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa3"), "scarf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa4"), "gloves"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa5"), "coat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa6"), "socks"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x97"), "dress"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x98"), "kimono"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbb"), "sari"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb1"), "one-piece swimsuit"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb2"), "briefs"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb3"), "shorts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x99"), "bikini"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9a"), "woman’s clothes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9b"), "purse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9c"), "handbag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9d"), "clutch bag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8d"), "shopping bags"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x92"), "backpack"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb4"), "thong sandal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9e"), "man’s shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9f"), "running shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbe"), "hiking boot"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbf"), "flat shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa0"), "high-heeled shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa1"), "woman’s sandal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb0"), "ballet shoes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa2"), "woman’s boot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x91"), "crown"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x92"), "woman’s hat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa9"), "top hat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x93"), "graduation cap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa2"), "billed cap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x96"), "military helmet"}, + Emoji{QString::fromUtf8("\xe2\x9b\x91"), "rescue worker’s helmet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbf"), "prayer beads"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x84"), "lipstick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8d"), "ring"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8e"), "gem stone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x87"), "muted speaker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x88"), "speaker low volume"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x89"), "speaker medium volume"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8a"), "speaker high volume"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa2"), "loudspeaker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa3"), "megaphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xaf"), "postal horn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x94"), "bell"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x95"), "bell with slash"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbc"), "musical score"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb5"), "musical note"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb6"), "musical notes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x99"), "studio microphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9a"), "level slider"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9b"), "control knobs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa4"), "microphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa7"), "headphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbb"), "radio"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb7"), "saxophone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x97"), "accordion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb8"), "guitar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb9"), "musical keyboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xba"), "trumpet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbb"), "violin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x95"), "banjo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x81"), "drum"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x98"), "long drum"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb1"), "mobile phone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb2"), "mobile phone with arrow"}, + Emoji{QString::fromUtf8("\xe2\x98\x8e"), "telephone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9e"), "telephone receiver"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9f"), "pager"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa0"), "fax machine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8b"), "battery"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8c"), "electric plug"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbb"), "laptop"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xa5"), "desktop computer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xa8"), "printer"}, + Emoji{QString::fromUtf8("\xe2\x8c\xa8"), "keyboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xb1"), "computer mouse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xb2"), "trackball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbd"), "computer disk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbe"), "floppy disk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbf"), "optical disk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x80"), "dvd"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xae"), "abacus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa5"), "movie camera"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9e"), "film frames"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbd"), "film projector"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xac"), "clapper board"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xba"), "television"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb7"), "camera"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb8"), "camera with flash"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb9"), "video camera"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbc"), "videocassette"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8d"), "magnifying glass tilted left"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8e"), "magnifying glass tilted right"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xaf"), "candle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa1"), "light bulb"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa6"), "flashlight"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xae"), "red paper lantern"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x94"), "diya lamp"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x94"), "notebook with decorative cover"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x95"), "closed book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x96"), "open book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x97"), "green book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x98"), "blue book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x99"), "orange book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9a"), "books"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x93"), "notebook"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x92"), "ledger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x83"), "page with curl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9c"), "scroll"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x84"), "page facing up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb0"), "newspaper"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x9e"), "rolled-up newspaper"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x91"), "bookmark tabs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x96"), "bookmark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb7"), "label"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb0"), "money bag"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x99"), "coin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb4"), "yen banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb5"), "dollar banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb6"), "euro banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb7"), "pound banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb8"), "money with wings"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb3"), "credit card"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbe"), "receipt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb9"), "chart increasing with yen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb1"), "currency exchange"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb2"), "heavy dollar sign"}, + Emoji{QString::fromUtf8("\xe2\x9c\x89"), "envelope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa7"), "e-mail"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa8"), "incoming envelope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa9"), "envelope with arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa4"), "outbox tray"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa5"), "inbox tray"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa6"), "package"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xab"), "closed mailbox with raised flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xaa"), "closed mailbox with lowered flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xac"), "open mailbox with raised flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xad"), "open mailbox with lowered flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xae"), "postbox"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xb3"), "ballot box with ballot"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8f"), "pencil"}, + Emoji{QString::fromUtf8("\xe2\x9c\x92"), "black nib"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8b"), "fountain pen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8a"), "pen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8c"), "paintbrush"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8d"), "crayon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9d"), "memo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbc"), "briefcase"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x81"), "file folder"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x82"), "open file folder"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x82"), "card index dividers"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x85"), "calendar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x86"), "tear-off calendar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x92"), "spiral notepad"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x93"), "spiral calendar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x87"), "card index"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x88"), "chart increasing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x89"), "chart decreasing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8a"), "bar chart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8b"), "clipboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8c"), "pushpin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8d"), "round pushpin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8e"), "paperclip"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x87"), "linked paperclips"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8f"), "straight ruler"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x90"), "triangular ruler"}, + Emoji{QString::fromUtf8("\xe2\x9c\x82"), "scissors"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x83"), "card file box"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x84"), "file cabinet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x91"), "wastebasket"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x92"), "locked"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x93"), "unlocked"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8f"), "locked with pen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x90"), "locked with key"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x91"), "key"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x9d"), "old key"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa8"), "hammer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x93"), "axe"}, + Emoji{QString::fromUtf8("\xe2\x9b\x8f"), "pick"}, + Emoji{QString::fromUtf8("\xe2\x9a\x92"), "hammer and pick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa0"), "hammer and wrench"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xa1"), "dagger"}, + Emoji{QString::fromUtf8("\xe2\x9a\x94"), "crossed swords"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xab"), "pistol"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x83"), "boomerang"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb9"), "bow and arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa1"), "shield"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9a"), "carpentry saw"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa7"), "wrench"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9b"), "screwdriver"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa9"), "nut and bolt"}, + Emoji{QString::fromUtf8("\xe2\x9a\x99"), "gear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x9c"), "clamp"}, + Emoji{QString::fromUtf8("\xe2\x9a\x96"), "balance scale"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xaf"), "probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x97"), "link"}, + Emoji{QString::fromUtf8("\xe2\x9b\x93"), "chains"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9d"), "hook"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb0"), "toolbox"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb2"), "magnet"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9c"), "ladder"}, + Emoji{QString::fromUtf8("\xe2\x9a\x97"), "alembic"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xaa"), "test tube"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xab"), "petri dish"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xac"), "dna"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xac"), "microscope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xad"), "telescope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa1"), "satellite antenna"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x89"), "syringe"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb8"), "drop of blood"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8a"), "pill"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb9"), "adhesive bandage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xba"), "stethoscope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaa"), "door"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x97"), "elevator"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9e"), "mirror"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9f"), "window"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8f"), "bed"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8b"), "couch and lamp"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x91"), "chair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbd"), "toilet"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa0"), "plunger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbf"), "shower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x81"), "bathtub"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa4"), "mouse trap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x92"), "razor"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb4"), "lotion bottle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb7"), "safety pin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb9"), "broom"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xba"), "basket"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbb"), "roll of paper"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa3"), "bucket"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbc"), "soap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa5"), "toothbrush"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbd"), "sponge"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xaf"), "fire extinguisher"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x92"), "shopping cart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xac"), "cigarette"}, + Emoji{QString::fromUtf8("\xe2\x9a\xb0"), "coffin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa6"), "headstone"}, + Emoji{QString::fromUtf8("\xe2\x9a\xb1"), "funeral urn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbf"), "moai"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa7"), "placard"}, }; -const std::vector<Emoji> Provider::symbols = { - Emoji{QString::fromUtf8("\xf0\x9f\x91\x81\xf0\x9f\x97\xa8"), ":eye_in_speech_bubble:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x98"), ":cupid:"}, - Emoji{QString::fromUtf8("\xe2\x9d\xa4"), ":heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x93"), ":heartbeat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x94"), ":broken_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x95"), ":two_hearts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x96"), ":sparkling_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x97"), ":heartpulse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x99"), ":blue_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9a"), ":green_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9b"), ":yellow_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9c"), ":purple_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xa4"), ":black_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9d"), ":gift_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9e"), ":revolving_hearts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9f"), ":heart_decoration:"}, - Emoji{QString::fromUtf8("\xe2\x9d\xa3"), ":heart_exclamation:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa2"), ":anger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa5"), ":boom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xab"), ":dizzy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xac"), ":speech_balloon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xa8"), ":speech_left:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xaf"), ":anger_right:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xad"), ":thought_balloon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xae"), ":white_flower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x90"), ":globe_with_meridians:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa8"), ":hotsprings:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x91"), ":octagonal_sign:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9b"), ":clock12:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa7"), ":clock1230:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x90"), ":clock1:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9c"), ":clock130:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x91"), ":clock2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9d"), ":clock230:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x92"), ":clock3:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9e"), ":clock330:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x93"), ":clock4:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9f"), ":clock430:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x94"), ":clock5:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa0"), ":clock530:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x95"), ":clock6:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa1"), ":clock630:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x96"), ":clock7:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa2"), ":clock730:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x97"), ":clock8:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa3"), ":clock830:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x98"), ":clock9:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa4"), ":clock930:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x99"), ":clock10:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa5"), ":clock1030:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9a"), ":clock11:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa6"), ":clock1130:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x80"), ":cyclone:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa0"), ":spades:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa5"), ":hearts:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa6"), ":diamonds:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa3"), ":clubs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x83\x8f"), ":black_joker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x80\x84"), ":mahjong:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb4"), ":flower_playing_cards:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x87"), ":mute:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x88"), ":speaker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x89"), ":sound:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8a"), ":loud_sound:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa2"), ":loudspeaker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa3"), ":mega:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x94"), ":bell:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x95"), ":no_bell:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb5"), ":musical_note:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb6"), ":notes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb9"), ":chart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb1"), ":currency_exchange:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb2"), ":heavy_dollar_sign:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa7"), ":atm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xae"), ":put_litter_in_its_place:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb0"), ":potable_water:"}, - Emoji{QString::fromUtf8("\xe2\x99\xbf"), ":wheelchair:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb9"), ":mens:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xba"), ":womens:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbb"), ":restroom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbc"), ":baby_symbol:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbe"), ":wc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x82"), ":passport_control:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x83"), ":customs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x84"), ":baggage_claim:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x85"), ":left_luggage:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xa0"), ":warning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb8"), ":children_crossing:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x94"), ":no_entry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xab"), ":no_entry_sign:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb3"), ":no_bicycles:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xad"), ":no_smoking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaf"), ":do_not_litter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb1"), ":non-potable_water:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb7"), ":no_pedestrians:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb5"), ":no_mobile_phones:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9e"), ":underage:"}, - Emoji{QString::fromUtf8("\xe2\x98\xa2"), ":radioactive:"}, - Emoji{QString::fromUtf8("\xe2\x98\xa3"), ":biohazard:"}, - Emoji{QString::fromUtf8("\xe2\xac\x86"), ":arrow_up:"}, - Emoji{QString::fromUtf8("\xe2\x86\x97"), ":arrow_upper_right:"}, - Emoji{QString::fromUtf8("\xe2\x9e\xa1"), ":arrow_right:"}, - Emoji{QString::fromUtf8("\xe2\x86\x98"), ":arrow_lower_right:"}, - Emoji{QString::fromUtf8("\xe2\xac\x87"), ":arrow_down:"}, - Emoji{QString::fromUtf8("\xe2\x86\x99"), ":arrow_lower_left:"}, - Emoji{QString::fromUtf8("\xe2\xac\x85"), ":arrow_left:"}, - Emoji{QString::fromUtf8("\xe2\x86\x96"), ":arrow_upper_left:"}, - Emoji{QString::fromUtf8("\xe2\x86\x95"), ":arrow_up_down:"}, - Emoji{QString::fromUtf8("\xe2\x86\x94"), ":left_right_arrow:"}, - Emoji{QString::fromUtf8("\xe2\x86\xa9"), ":leftwards_arrow_with_hook:"}, - Emoji{QString::fromUtf8("\xe2\x86\xaa"), ":arrow_right_hook:"}, - Emoji{QString::fromUtf8("\xe2\xa4\xb4"), ":arrow_heading_up:"}, - Emoji{QString::fromUtf8("\xe2\xa4\xb5"), ":arrow_heading_down:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x83"), ":arrows_clockwise:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x84"), ":arrows_counterclockwise:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x99"), ":back:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9a"), ":end:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9b"), ":on:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9c"), ":soon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9d"), ":top:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x90"), ":place_of_worship:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x9b"), ":atom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x89"), ":om_symbol:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xa1"), ":star_of_david:"}, - Emoji{QString::fromUtf8("\xe2\x98\xb8"), ":wheel_of_dharma:"}, - Emoji{QString::fromUtf8("\xe2\x98\xaf"), ":yin_yang:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x9d"), ":cross:"}, - Emoji{QString::fromUtf8("\xe2\x98\xa6"), ":orthodox_cross:"}, - Emoji{QString::fromUtf8("\xe2\x98\xaa"), ":star_and_crescent:"}, - Emoji{QString::fromUtf8("\xe2\x98\xae"), ":peace:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8e"), ":menorah:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xaf"), ":six_pointed_star:"}, - Emoji{QString::fromUtf8("\xe2\x99\x88"), ":aries:"}, - Emoji{QString::fromUtf8("\xe2\x99\x89"), ":taurus:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8a"), ":gemini:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8b"), ":cancer:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8c"), ":leo:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8d"), ":virgo:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8e"), ":libra:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8f"), ":scorpius:"}, - Emoji{QString::fromUtf8("\xe2\x99\x90"), ":sagittarius:"}, - Emoji{QString::fromUtf8("\xe2\x99\x91"), ":capricorn:"}, - Emoji{QString::fromUtf8("\xe2\x99\x92"), ":aquarius:"}, - Emoji{QString::fromUtf8("\xe2\x99\x93"), ":pisces:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x8e"), ":ophiuchus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x80"), ":twisted_rightwards_arrows:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x81"), ":repeat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x82"), ":repeat_one:"}, - Emoji{QString::fromUtf8("\xe2\x96\xb6"), ":arrow_forward:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xa9"), ":fast_forward:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xad"), ":track_next:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xaf"), ":play_pause:"}, - Emoji{QString::fromUtf8("\xe2\x97\x80"), ":arrow_backward:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xaa"), ":rewind:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xae"), ":track_previous:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xbc"), ":arrow_up_small:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xab"), ":arrow_double_up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xbd"), ":arrow_down_small:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xac"), ":arrow_double_down:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb8"), ":pause_button:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb9"), ":stop_button:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xba"), ":record_button:"}, - Emoji{QString::fromUtf8("\xe2\x8f\x8f"), ":eject:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa6"), ":cinema:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x85"), ":low_brightness:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x86"), ":high_brightness:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb6"), ":signal_strength:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb3"), ":vibration_mode:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb4"), ":mobile_phone_off:"}, - Emoji{QString::fromUtf8("\xe2\x99\xbb"), ":recycle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9b"), ":name_badge:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x9c"), ":fleur-de-lis:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb0"), ":beginner:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb1"), ":trident:"}, - Emoji{QString::fromUtf8("\xe2\xad\x95"), ":o:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x85"), ":white_check_mark:"}, - Emoji{QString::fromUtf8("\xe2\x98\x91"), ":ballot_box_with_check:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x94"), ":heavy_check_mark:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x96"), ":heavy_multiplication_x:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x8c"), ":x:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x8e"), ":negative_squared_cross_mark:"}, - Emoji{QString::fromUtf8("\xe2\x9e\x95"), ":heavy_plus_sign:"}, - Emoji{QString::fromUtf8("\xe2\x9e\x96"), ":heavy_minus_sign:"}, - Emoji{QString::fromUtf8("\xe2\x9e\x97"), ":heavy_division_sign:"}, - Emoji{QString::fromUtf8("\xe2\x9e\xb0"), ":curly_loop:"}, - Emoji{QString::fromUtf8("\xe2\x9e\xbf"), ":loop:"}, - Emoji{QString::fromUtf8("\xe3\x80\xbd"), ":part_alternation_mark:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xb3"), ":eight_spoked_asterisk:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xb4"), ":eight_pointed_black_star:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x87"), ":sparkle:"}, - Emoji{QString::fromUtf8("\xe2\x80\xbc"), ":bangbang:"}, - Emoji{QString::fromUtf8("\xe2\x81\x89"), ":interrobang:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x93"), ":question:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x94"), ":grey_question:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x95"), ":grey_exclamation:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x97"), ":exclamation:"}, - Emoji{QString::fromUtf8("\xe3\x80\xb0"), ":wavy_dash:"}, - Emoji{QString::fromUtf8("\xc2\xa9"), ":copyright:"}, - Emoji{QString::fromUtf8("\xc2\xae"), ":registered:"}, - Emoji{QString::fromUtf8("\xe2\x84\xa2"), ":tm:"}, - Emoji{QString::fromUtf8("#\xe2\x83\xa3"), ":hash:"}, - Emoji{QString::fromUtf8("*\xe2\x83\xa3"), ":asterisk:"}, - Emoji{QString::fromUtf8("0\xe2\x83\xa3"), ":zero:"}, - Emoji{QString::fromUtf8("1\xe2\x83\xa3"), ":one:"}, - Emoji{QString::fromUtf8("2\xe2\x83\xa3"), ":two:"}, - Emoji{QString::fromUtf8("3\xe2\x83\xa3"), ":three:"}, - Emoji{QString::fromUtf8("4\xe2\x83\xa3"), ":four:"}, - Emoji{QString::fromUtf8("5\xe2\x83\xa3"), ":five:"}, - Emoji{QString::fromUtf8("6\xe2\x83\xa3"), ":six:"}, - Emoji{QString::fromUtf8("7\xe2\x83\xa3"), ":seven:"}, - Emoji{QString::fromUtf8("8\xe2\x83\xa3"), ":eight:"}, - Emoji{QString::fromUtf8("9\xe2\x83\xa3"), ":nine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9f"), ":keycap_ten:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xaf"), ":100:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa0"), ":capital_abcd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa1"), ":abcd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa2"), ":1234:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa3"), ":symbols:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa4"), ":abc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xb0"), ":a:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x8e"), ":ab:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xb1"), ":b:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x91"), ":cl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x92"), ":cool:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x93"), ":free:"}, - Emoji{QString::fromUtf8("\xe2\x84\xb9"), ":information_source:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x94"), ":id:"}, - Emoji{QString::fromUtf8("\xe2\x93\x82"), ":m:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x95"), ":new:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x96"), ":ng:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xbe"), ":o2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x97"), ":ok:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xbf"), ":parking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x98"), ":sos:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x99"), ":up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x9a"), ":vs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\x81"), ":koko:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\x82"), ":sa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb7"), ":u6708:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb6"), ":u6709:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xaf"), ":u6307:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x89\x90"), ":ideograph_advantage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb9"), ":u5272:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\x9a"), ":u7121:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb2"), ":u7981:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x89\x91"), ":accept:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb8"), ":u7533:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb4"), ":u5408:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb3"), ":u7a7a:"}, - Emoji{QString::fromUtf8("\xe3\x8a\x97"), ":congratulations:"}, - Emoji{QString::fromUtf8("\xe3\x8a\x99"), ":secret:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xba"), ":u55b6:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb5"), ":u6e80:"}, - Emoji{QString::fromUtf8("\xe2\x96\xaa"), ":black_small_square:"}, - Emoji{QString::fromUtf8("\xe2\x96\xab"), ":white_small_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbb"), ":white_medium_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbc"), ":black_medium_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbd"), ":white_medium_small_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbe"), ":black_medium_small_square:"}, - Emoji{QString::fromUtf8("\xe2\xac\x9b"), ":black_large_square:"}, - Emoji{QString::fromUtf8("\xe2\xac\x9c"), ":white_large_square:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb6"), ":large_orange_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb7"), ":large_blue_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb8"), ":small_orange_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb9"), ":small_blue_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xba"), ":small_red_triangle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xbb"), ":small_red_triangle_down:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa0"), ":diamond_shape_with_a_dot_inside:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x98"), ":radio_button:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb2"), ":black_square_button:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb3"), ":white_square_button:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xaa"), ":white_circle:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xab"), ":black_circle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb4"), ":red_circle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb5"), ":blue_circle:"}, +const std::vector<Emoji> emoji::Provider::symbols = { + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa7"), "ATM sign"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xae"), "litter in bin sign"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb0"), "potable water"}, + Emoji{QString::fromUtf8("\xe2\x99\xbf"), "wheelchair symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb9"), "men’s room"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xba"), "women’s room"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbb"), "restroom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbc"), "baby symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbe"), "water closet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x82"), "passport control"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x83"), "customs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x84"), "baggage claim"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x85"), "left luggage"}, + Emoji{QString::fromUtf8("\xe2\x9a\xa0"), "warning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb8"), "children crossing"}, + Emoji{QString::fromUtf8("\xe2\x9b\x94"), "no entry"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xab"), "prohibited"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb3"), "no bicycles"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xad"), "no smoking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaf"), "no littering"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb1"), "non-potable water"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb7"), "no pedestrians"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb5"), "no mobile phones"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9e"), "no one under eighteen"}, + Emoji{QString::fromUtf8("\xe2\x98\xa2"), "radioactive"}, + Emoji{QString::fromUtf8("\xe2\x98\xa3"), "biohazard"}, + Emoji{QString::fromUtf8("\xe2\xac\x86"), "up arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x97"), "up-right arrow"}, + Emoji{QString::fromUtf8("\xe2\x9e\xa1"), "right arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x98"), "down-right arrow"}, + Emoji{QString::fromUtf8("\xe2\xac\x87"), "down arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x99"), "down-left arrow"}, + Emoji{QString::fromUtf8("\xe2\xac\x85"), "left arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x96"), "up-left arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x95"), "up-down arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x94"), "left-right arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\xa9"), "right arrow curving left"}, + Emoji{QString::fromUtf8("\xe2\x86\xaa"), "left arrow curving right"}, + Emoji{QString::fromUtf8("\xe2\xa4\xb4"), "right arrow curving up"}, + Emoji{QString::fromUtf8("\xe2\xa4\xb5"), "right arrow curving down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x83"), "clockwise vertical arrows"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x84"), "counterclockwise arrows button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x99"), "BACK arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9a"), "END arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9b"), "ON! arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9c"), "SOON arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9d"), "TOP arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x90"), "place of worship"}, + Emoji{QString::fromUtf8("\xe2\x9a\x9b"), "atom symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x89"), "om"}, + Emoji{QString::fromUtf8("\xe2\x9c\xa1"), "star of David"}, + Emoji{QString::fromUtf8("\xe2\x98\xb8"), "wheel of dharma"}, + Emoji{QString::fromUtf8("\xe2\x98\xaf"), "yin yang"}, + Emoji{QString::fromUtf8("\xe2\x9c\x9d"), "latin cross"}, + Emoji{QString::fromUtf8("\xe2\x98\xa6"), "orthodox cross"}, + Emoji{QString::fromUtf8("\xe2\x98\xaa"), "star and crescent"}, + Emoji{QString::fromUtf8("\xe2\x98\xae"), "peace symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8e"), "menorah"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xaf"), "dotted six-pointed star"}, + Emoji{QString::fromUtf8("\xe2\x99\x88"), "Aries"}, + Emoji{QString::fromUtf8("\xe2\x99\x89"), "Taurus"}, + Emoji{QString::fromUtf8("\xe2\x99\x8a"), "Gemini"}, + Emoji{QString::fromUtf8("\xe2\x99\x8b"), "Cancer"}, + Emoji{QString::fromUtf8("\xe2\x99\x8c"), "Leo"}, + Emoji{QString::fromUtf8("\xe2\x99\x8d"), "Virgo"}, + Emoji{QString::fromUtf8("\xe2\x99\x8e"), "Libra"}, + Emoji{QString::fromUtf8("\xe2\x99\x8f"), "Scorpio"}, + Emoji{QString::fromUtf8("\xe2\x99\x90"), "Sagittarius"}, + Emoji{QString::fromUtf8("\xe2\x99\x91"), "Capricorn"}, + Emoji{QString::fromUtf8("\xe2\x99\x92"), "Aquarius"}, + Emoji{QString::fromUtf8("\xe2\x99\x93"), "Pisces"}, + Emoji{QString::fromUtf8("\xe2\x9b\x8e"), "Ophiuchus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x80"), "shuffle tracks button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x81"), "repeat button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x82"), "repeat single button"}, + Emoji{QString::fromUtf8("\xe2\x96\xb6"), "play button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xa9"), "fast-forward button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xad"), "next track button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xaf"), "play or pause button"}, + Emoji{QString::fromUtf8("\xe2\x97\x80"), "reverse button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xaa"), "fast reverse button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xae"), "last track button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xbc"), "upwards button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xab"), "fast up button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xbd"), "downwards button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xac"), "fast down button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb8"), "pause button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb9"), "stop button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xba"), "record button"}, + Emoji{QString::fromUtf8("\xe2\x8f\x8f"), "eject button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa6"), "cinema"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x85"), "dim button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x86"), "bright button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb6"), "antenna bars"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb3"), "vibration mode"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb4"), "mobile phone off"}, + Emoji{QString::fromUtf8("\xe2\x99\x80"), "female sign"}, + Emoji{QString::fromUtf8("\xe2\x99\x82"), "male sign"}, + Emoji{QString::fromUtf8("\xe2\x9a\xa7"), "transgender symbol"}, + Emoji{QString::fromUtf8("\xe2\x9a\x95"), "medical symbol"}, + Emoji{QString::fromUtf8("\xe2\x99\xbe"), "infinity"}, + Emoji{QString::fromUtf8("\xe2\x99\xbb"), "recycling symbol"}, + Emoji{QString::fromUtf8("\xe2\x9a\x9c"), "fleur-de-lis"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb1"), "trident emblem"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9b"), "name badge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb0"), "Japanese symbol for beginner"}, + Emoji{QString::fromUtf8("\xe2\xad\x95"), "hollow red circle"}, + Emoji{QString::fromUtf8("\xe2\x9c\x85"), "check mark button"}, + Emoji{QString::fromUtf8("\xe2\x98\x91"), "check box with check"}, + Emoji{QString::fromUtf8("\xe2\x9c\x94"), "check mark"}, + Emoji{QString::fromUtf8("\xe2\x9c\x96"), "multiplication sign"}, + Emoji{QString::fromUtf8("\xe2\x9d\x8c"), "cross mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x8e"), "cross mark button"}, + Emoji{QString::fromUtf8("\xe2\x9e\x95"), "plus sign"}, + Emoji{QString::fromUtf8("\xe2\x9e\x96"), "minus sign"}, + Emoji{QString::fromUtf8("\xe2\x9e\x97"), "division sign"}, + Emoji{QString::fromUtf8("\xe2\x9e\xb0"), "curly loop"}, + Emoji{QString::fromUtf8("\xe2\x9e\xbf"), "double curly loop"}, + Emoji{QString::fromUtf8("\xe3\x80\xbd"), "part alternation mark"}, + Emoji{QString::fromUtf8("\xe2\x9c\xb3"), "eight-spoked asterisk"}, + Emoji{QString::fromUtf8("\xe2\x9c\xb4"), "eight-pointed star"}, + Emoji{QString::fromUtf8("\xe2\x9d\x87"), "sparkle"}, + Emoji{QString::fromUtf8("\xe2\x80\xbc"), "double exclamation mark"}, + Emoji{QString::fromUtf8("\xe2\x81\x89"), "exclamation question mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x93"), "question mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x94"), "white question mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x95"), "white exclamation mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x97"), "exclamation mark"}, + Emoji{QString::fromUtf8("\xe3\x80\xb0"), "wavy dash"}, + Emoji{QString::fromUtf8("\xc2\xa9"), "copyright"}, + Emoji{QString::fromUtf8("\xc2\xae"), "registered"}, + Emoji{QString::fromUtf8("\xe2\x84\xa2"), "trade mark"}, + Emoji{QString::fromUtf8("#\xef\xb8\x8f\xe2\x83\xa3"), "keycap: #"}, + Emoji{QString::fromUtf8("#\xe2\x83\xa3"), "keycap: #"}, + Emoji{QString::fromUtf8("*\xef\xb8\x8f\xe2\x83\xa3"), "keycap: *"}, + Emoji{QString::fromUtf8("*\xe2\x83\xa3"), "keycap: *"}, + Emoji{QString::fromUtf8("0\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 0"}, + Emoji{QString::fromUtf8("0\xe2\x83\xa3"), "keycap: 0"}, + Emoji{QString::fromUtf8("1\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 1"}, + Emoji{QString::fromUtf8("1\xe2\x83\xa3"), "keycap: 1"}, + Emoji{QString::fromUtf8("2\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 2"}, + Emoji{QString::fromUtf8("2\xe2\x83\xa3"), "keycap: 2"}, + Emoji{QString::fromUtf8("3\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 3"}, + Emoji{QString::fromUtf8("3\xe2\x83\xa3"), "keycap: 3"}, + Emoji{QString::fromUtf8("4\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 4"}, + Emoji{QString::fromUtf8("4\xe2\x83\xa3"), "keycap: 4"}, + Emoji{QString::fromUtf8("5\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 5"}, + Emoji{QString::fromUtf8("5\xe2\x83\xa3"), "keycap: 5"}, + Emoji{QString::fromUtf8("6\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 6"}, + Emoji{QString::fromUtf8("6\xe2\x83\xa3"), "keycap: 6"}, + Emoji{QString::fromUtf8("7\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 7"}, + Emoji{QString::fromUtf8("7\xe2\x83\xa3"), "keycap: 7"}, + Emoji{QString::fromUtf8("8\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 8"}, + Emoji{QString::fromUtf8("8\xe2\x83\xa3"), "keycap: 8"}, + Emoji{QString::fromUtf8("9\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 9"}, + Emoji{QString::fromUtf8("9\xe2\x83\xa3"), "keycap: 9"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9f"), "keycap: 10"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa0"), "input latin uppercase"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa1"), "input latin lowercase"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa2"), "input numbers"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa3"), "input symbols"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa4"), "input latin letters"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xb0"), "A button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x8e"), "AB button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xb1"), "B button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x91"), "CL button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x92"), "COOL button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x93"), "FREE button"}, + Emoji{QString::fromUtf8("\xe2\x84\xb9"), "information"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x94"), "ID button"}, + Emoji{QString::fromUtf8("\xe2\x93\x82"), "circled M"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x95"), "NEW button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x96"), "NG button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xbe"), "O button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x97"), "OK button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xbf"), "P button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x98"), "SOS button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x99"), "UP! button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x9a"), "VS button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\x81"), "Japanese “here” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\x82"), "Japanese “service charge” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb7"), "Japanese “monthly amount” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb6"), "Japanese “not free of charge” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xaf"), "Japanese “reserved” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x89\x90"), "Japanese “bargain” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb9"), "Japanese “discount” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\x9a"), "Japanese “free of charge” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb2"), "Japanese “prohibited” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x89\x91"), "Japanese “acceptable” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb8"), "Japanese “application” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb4"), "Japanese “passing grade” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb3"), "Japanese “vacancy” button"}, + Emoji{QString::fromUtf8("\xe3\x8a\x97"), "Japanese “congratulations” button"}, + Emoji{QString::fromUtf8("\xe3\x8a\x99"), "Japanese “secret” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xba"), "Japanese “open for business” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb5"), "Japanese “no vacancy” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb4"), "red circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa0"), "orange circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa1"), "yellow circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa2"), "green circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb5"), "blue circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa3"), "purple circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa4"), "brown circle"}, + Emoji{QString::fromUtf8("\xe2\x9a\xab"), "black circle"}, + Emoji{QString::fromUtf8("\xe2\x9a\xaa"), "white circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa5"), "red square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa7"), "orange square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa8"), "yellow square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa9"), "green square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa6"), "blue square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xaa"), "purple square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xab"), "brown square"}, + Emoji{QString::fromUtf8("\xe2\xac\x9b"), "black large square"}, + Emoji{QString::fromUtf8("\xe2\xac\x9c"), "white large square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbc"), "black medium square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbb"), "white medium square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbe"), "black medium-small square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbd"), "white medium-small square"}, + Emoji{QString::fromUtf8("\xe2\x96\xaa"), "black small square"}, + Emoji{QString::fromUtf8("\xe2\x96\xab"), "white small square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb6"), "large orange diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb7"), "large blue diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb8"), "small orange diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb9"), "small blue diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xba"), "red triangle pointed up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xbb"), "red triangle pointed down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa0"), "diamond with a dot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x98"), "radio button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb3"), "white square button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb2"), "black square button"}, }; -const std::vector<Emoji> Provider::flags = { - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa8"), ":flag_ac:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa9"), ":flag_ad:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xaa"), ":flag_ae:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xab"), ":flag_af:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xac"), ":flag_ag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xae"), ":flag_ai:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb1"), ":flag_al:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2"), ":flag_am:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb4"), ":flag_ao:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb6"), ":flag_aq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb7"), ":flag_ar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb8"), ":flag_as:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb9"), ":flag_at:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xba"), ":flag_au:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbc"), ":flag_aw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbd"), ":flag_ax:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbf"), ":flag_az:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa6"), ":flag_ba:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa7"), ":flag_bb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa9"), ":flag_bd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaa"), ":flag_be:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xab"), ":flag_bf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xac"), ":flag_bg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xad"), ":flag_bh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xae"), ":flag_bi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaf"), ":flag_bj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb1"), ":flag_bl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb2"), ":flag_bm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb3"), ":flag_bn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb4"), ":flag_bo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb6"), ":flag_bq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb7"), ":flag_br:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb8"), ":flag_bs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb9"), ":flag_bt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbb"), ":flag_bv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbc"), ":flag_bw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbe"), ":flag_by:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbf"), ":flag_bz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa6"), ":flag_ca:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa8"), ":flag_cc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa9"), ":flag_cd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xab"), ":flag_cf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xac"), ":flag_cg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xad"), ":flag_ch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xae"), ":flag_ci:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb0"), ":flag_ck:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb1"), ":flag_cl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb2"), ":flag_cm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb3"), ":flag_cn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb4"), ":flag_co:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb5"), ":flag_cp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb7"), ":flag_cr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xba"), ":flag_cu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbb"), ":flag_cv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbc"), ":flag_cw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbd"), ":flag_cx:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbe"), ":flag_cy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbf"), ":flag_cz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaa"), ":flag_de:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xac"), ":flag_dg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaf"), ":flag_dj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb0"), ":flag_dk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb2"), ":flag_dm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb4"), ":flag_do:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xbf"), ":flag_dz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa6"), ":flag_ea:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa8"), ":flag_ec:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xaa"), ":flag_ee:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xac"), ":flag_eg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xad"), ":flag_eh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb7"), ":flag_er:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb8"), ":flag_es:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb9"), ":flag_et:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xba"), ":flag_eu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xae"), ":flag_fi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xaf"), ":flag_fj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb0"), ":flag_fk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb2"), ":flag_fm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb4"), ":flag_fo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb7"), ":flag_fr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa6"), ":flag_ga:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa7"), ":flag_gb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa9"), ":flag_gd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xaa"), ":flag_ge:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xab"), ":flag_gf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xac"), ":flag_gg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xad"), ":flag_gh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xae"), ":flag_gi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb1"), ":flag_gl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb2"), ":flag_gm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb3"), ":flag_gn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb5"), ":flag_gp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb6"), ":flag_gq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb7"), ":flag_gr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb8"), ":flag_gs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb9"), ":flag_gt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xba"), ":flag_gu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbc"), ":flag_gw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbe"), ":flag_gy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb0"), ":flag_hk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb2"), ":flag_hm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb3"), ":flag_hn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb7"), ":flag_hr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb9"), ":flag_ht:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xba"), ":flag_hu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa8"), ":flag_ic:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa9"), ":flag_id:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xaa"), ":flag_ie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb1"), ":flag_il:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb2"), ":flag_im:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb3"), ":flag_in:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb4"), ":flag_io:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb6"), ":flag_iq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb7"), ":flag_ir:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb8"), ":flag_is:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb9"), ":flag_it:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xaa"), ":flag_je:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb2"), ":flag_jm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb4"), ":flag_jo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb5"), ":flag_jp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xaa"), ":flag_ke:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xac"), ":flag_kg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xad"), ":flag_kh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xae"), ":flag_ki:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb2"), ":flag_km:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb3"), ":flag_kn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb5"), ":flag_kp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb7"), ":flag_kr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbc"), ":flag_kw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbe"), ":flag_ky:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbf"), ":flag_kz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa6"), ":flag_la:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa7"), ":flag_lb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa8"), ":flag_lc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xae"), ":flag_li:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb0"), ":flag_lk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb7"), ":flag_lr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb8"), ":flag_ls:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb9"), ":flag_lt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xba"), ":flag_lu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbb"), ":flag_lv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbe"), ":flag_ly:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa6"), ":flag_ma:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa8"), ":flag_mc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa9"), ":flag_md:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xaa"), ":flag_me:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xab"), ":flag_mf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xac"), ":flag_mg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xad"), ":flag_mh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb0"), ":flag_mk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb1"), ":flag_ml:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb2"), ":flag_mm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb3"), ":flag_mn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb4"), ":flag_mo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb5"), ":flag_mp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb6"), ":flag_mq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb7"), ":flag_mr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb8"), ":flag_ms:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb9"), ":flag_mt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xba"), ":flag_mu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbb"), ":flag_mv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbc"), ":flag_mw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbd"), ":flag_mx:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbe"), ":flag_my:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbf"), ":flag_mz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa6"), ":flag_na:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa8"), ":flag_nc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xaa"), ":flag_ne:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xab"), ":flag_nf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xac"), ":flag_ng:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xae"), ":flag_ni:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb1"), ":flag_nl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb4"), ":flag_no:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb5"), ":flag_np:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb7"), ":flag_nr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xba"), ":flag_nu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xbf"), ":flag_nz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb4\xf0\x9f\x87\xb2"), ":flag_om:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xa6"), ":flag_pa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xaa"), ":flag_pe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xab"), ":flag_pf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xac"), ":flag_pg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xad"), ":flag_ph:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb0"), ":flag_pk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb1"), ":flag_pl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb2"), ":flag_pm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb3"), ":flag_pn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb7"), ":flag_pr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb8"), ":flag_ps:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb9"), ":flag_pt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbc"), ":flag_pw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbe"), ":flag_py:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb6\xf0\x9f\x87\xa6"), ":flag_qa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xaa"), ":flag_re:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb4"), ":flag_ro:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb8"), ":flag_rs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xba"), ":flag_ru:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xbc"), ":flag_rw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6"), ":flag_sa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa7"), ":flag_sb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa8"), ":flag_sc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa9"), ":flag_sd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaa"), ":flag_se:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xac"), ":flag_sg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xad"), ":flag_sh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xae"), ":flag_si:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaf"), ":flag_sj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb0"), ":flag_sk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb1"), ":flag_sl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb2"), ":flag_sm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb3"), ":flag_sn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb4"), ":flag_so:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7"), ":flag_sr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb8"), ":flag_ss:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb9"), ":flag_st:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbb"), ":flag_sv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbd"), ":flag_sx:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbe"), ":flag_sy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbf"), ":flag_sz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa6"), ":flag_ta:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa8"), ":flag_tc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa9"), ":flag_td:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xab"), ":flag_tf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xac"), ":flag_tg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xad"), ":flag_th:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xaf"), ":flag_tj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb0"), ":flag_tk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb1"), ":flag_tl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb2"), ":flag_tm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb3"), ":flag_tn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb4"), ":flag_to:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb7"), ":flag_tr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb9"), ":flag_tt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbb"), ":flag_tv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbc"), ":flag_tw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbf"), ":flag_tz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xa6"), ":flag_ua:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xac"), ":flag_ug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb2"), ":flag_um:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb8"), ":flag_us:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbe"), ":flag_uy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbf"), ":flag_uz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa6"), ":flag_va:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa8"), ":flag_vc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xaa"), ":flag_ve:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xac"), ":flag_vg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xae"), ":flag_vi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xb3"), ":flag_vn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xba"), ":flag_vu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xab"), ":flag_wf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xb8"), ":flag_ws:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbd\xf0\x9f\x87\xb0"), ":flag_xk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xaa"), ":flag_ye:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xb9"), ":flag_yt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xa6"), ":flag_za:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xb2"), ":flag_zm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xbc"), ":flag_zw:"}, +const std::vector<Emoji> emoji::Provider::flags = { + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x81"), "chequered flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa9"), "triangular flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8c"), "crossed flags"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4"), "black flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3"), "white flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x8c\x88"), + "rainbow flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xe2\x80\x8d\xf0\x9f\x8c\x88"), "rainbow flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xe2\x9a\xa7"), + "transgender flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xe2\x80\x8d\xe2\x9a\xa7"), "transgender flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xe2\x80\x8d\xe2\x98\xa0"), "pirate flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa8"), "flag: Ascension Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa9"), "flag: Andorra"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xaa"), "flag: United Arab Emirates"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xab"), "flag: Afghanistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xac"), "flag: Antigua & Barbuda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xae"), "flag: Anguilla"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb1"), "flag: Albania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2"), "flag: Armenia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb4"), "flag: Angola"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb6"), "flag: Antarctica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb7"), "flag: Argentina"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb8"), "flag: American Samoa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb9"), "flag: Austria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xba"), "flag: Australia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbc"), "flag: Aruba"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbd"), "flag: Åland Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbf"), "flag: Azerbaijan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa6"), "flag: Bosnia & Herzegovina"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa7"), "flag: Barbados"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa9"), "flag: Bangladesh"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaa"), "flag: Belgium"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xab"), "flag: Burkina Faso"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xac"), "flag: Bulgaria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xad"), "flag: Bahrain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xae"), "flag: Burundi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaf"), "flag: Benin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb1"), "flag: St. Barthélemy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb2"), "flag: Bermuda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb3"), "flag: Brunei"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb4"), "flag: Bolivia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb6"), "flag: Caribbean Netherlands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb7"), "flag: Brazil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb8"), "flag: Bahamas"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb9"), "flag: Bhutan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbb"), "flag: Bouvet Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbc"), "flag: Botswana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbe"), "flag: Belarus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbf"), "flag: Belize"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa6"), "flag: Canada"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa8"), "flag: Cocos (Keeling) Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa9"), "flag: Congo - Kinshasa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xab"), "flag: Central African Republic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xac"), "flag: Congo - Brazzaville"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xad"), "flag: Switzerland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xae"), "flag: Côte d’Ivoire"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb0"), "flag: Cook Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb1"), "flag: Chile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb2"), "flag: Cameroon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb3"), "flag: China"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb4"), "flag: Colombia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb5"), "flag: Clipperton Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb7"), "flag: Costa Rica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xba"), "flag: Cuba"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbb"), "flag: Cape Verde"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbc"), "flag: Curaçao"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbd"), "flag: Christmas Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbe"), "flag: Cyprus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbf"), "flag: Czechia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaa"), "flag: Germany"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xac"), "flag: Diego Garcia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaf"), "flag: Djibouti"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb0"), "flag: Denmark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb2"), "flag: Dominica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb4"), "flag: Dominican Republic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xbf"), "flag: Algeria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa6"), "flag: Ceuta & Melilla"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa8"), "flag: Ecuador"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xaa"), "flag: Estonia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xac"), "flag: Egypt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xad"), "flag: Western Sahara"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb7"), "flag: Eritrea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb8"), "flag: Spain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb9"), "flag: Ethiopia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xba"), "flag: European Union"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xae"), "flag: Finland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xaf"), "flag: Fiji"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb0"), "flag: Falkland Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb2"), "flag: Micronesia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb4"), "flag: Faroe Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb7"), "flag: France"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa6"), "flag: Gabon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa7"), "flag: United Kingdom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa9"), "flag: Grenada"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xaa"), "flag: Georgia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xab"), "flag: French Guiana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xac"), "flag: Guernsey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xad"), "flag: Ghana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xae"), "flag: Gibraltar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb1"), "flag: Greenland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb2"), "flag: Gambia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb3"), "flag: Guinea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb5"), "flag: Guadeloupe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb6"), "flag: Equatorial Guinea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb7"), "flag: Greece"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb8"), + "flag: South Georgia & South Sandwich Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb9"), "flag: Guatemala"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xba"), "flag: Guam"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbc"), "flag: Guinea-Bissau"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbe"), "flag: Guyana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb0"), "flag: Hong Kong SAR China"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb2"), "flag: Heard & McDonald Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb3"), "flag: Honduras"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb7"), "flag: Croatia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb9"), "flag: Haiti"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xba"), "flag: Hungary"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa8"), "flag: Canary Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa9"), "flag: Indonesia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xaa"), "flag: Ireland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb1"), "flag: Israel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb2"), "flag: Isle of Man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb3"), "flag: India"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb4"), + "flag: British Indian Ocean Territory"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb6"), "flag: Iraq"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb7"), "flag: Iran"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb8"), "flag: Iceland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb9"), "flag: Italy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xaa"), "flag: Jersey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb2"), "flag: Jamaica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb4"), "flag: Jordan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb5"), "flag: Japan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xaa"), "flag: Kenya"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xac"), "flag: Kyrgyzstan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xad"), "flag: Cambodia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xae"), "flag: Kiribati"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb2"), "flag: Comoros"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb3"), "flag: St. Kitts & Nevis"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb5"), "flag: North Korea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb7"), "flag: South Korea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbc"), "flag: Kuwait"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbe"), "flag: Cayman Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbf"), "flag: Kazakhstan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa6"), "flag: Laos"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa7"), "flag: Lebanon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa8"), "flag: St. Lucia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xae"), "flag: Liechtenstein"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb0"), "flag: Sri Lanka"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb7"), "flag: Liberia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb8"), "flag: Lesotho"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb9"), "flag: Lithuania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xba"), "flag: Luxembourg"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbb"), "flag: Latvia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbe"), "flag: Libya"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa6"), "flag: Morocco"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa8"), "flag: Monaco"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa9"), "flag: Moldova"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xaa"), "flag: Montenegro"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xab"), "flag: St. Martin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xac"), "flag: Madagascar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xad"), "flag: Marshall Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb0"), "flag: North Macedonia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb1"), "flag: Mali"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb2"), "flag: Myanmar (Burma)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb3"), "flag: Mongolia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb4"), "flag: Macao SAR China"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb5"), "flag: Northern Mariana Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb6"), "flag: Martinique"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb7"), "flag: Mauritania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb8"), "flag: Montserrat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb9"), "flag: Malta"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xba"), "flag: Mauritius"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbb"), "flag: Maldives"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbc"), "flag: Malawi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbd"), "flag: Mexico"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbe"), "flag: Malaysia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbf"), "flag: Mozambique"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa6"), "flag: Namibia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa8"), "flag: New Caledonia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xaa"), "flag: Niger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xab"), "flag: Norfolk Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xac"), "flag: Nigeria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xae"), "flag: Nicaragua"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb1"), "flag: Netherlands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb4"), "flag: Norway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb5"), "flag: Nepal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb7"), "flag: Nauru"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xba"), "flag: Niue"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xbf"), "flag: New Zealand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb4\xf0\x9f\x87\xb2"), "flag: Oman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xa6"), "flag: Panama"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xaa"), "flag: Peru"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xab"), "flag: French Polynesia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xac"), "flag: Papua New Guinea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xad"), "flag: Philippines"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb0"), "flag: Pakistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb1"), "flag: Poland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb2"), "flag: St. Pierre & Miquelon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb3"), "flag: Pitcairn Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb7"), "flag: Puerto Rico"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb8"), "flag: Palestinian Territories"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb9"), "flag: Portugal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbc"), "flag: Palau"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbe"), "flag: Paraguay"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb6\xf0\x9f\x87\xa6"), "flag: Qatar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xaa"), "flag: Réunion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb4"), "flag: Romania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb8"), "flag: Serbia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xba"), "flag: Russia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xbc"), "flag: Rwanda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6"), "flag: Saudi Arabia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa7"), "flag: Solomon Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa8"), "flag: Seychelles"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa9"), "flag: Sudan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaa"), "flag: Sweden"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xac"), "flag: Singapore"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xad"), "flag: St. Helena"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xae"), "flag: Slovenia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaf"), "flag: Svalbard & Jan Mayen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb0"), "flag: Slovakia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb1"), "flag: Sierra Leone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb2"), "flag: San Marino"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb3"), "flag: Senegal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb4"), "flag: Somalia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7"), "flag: Suriname"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb8"), "flag: South Sudan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb9"), "flag: São Tomé & Príncipe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbb"), "flag: El Salvador"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbd"), "flag: Sint Maarten"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbe"), "flag: Syria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbf"), "flag: Eswatini"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa6"), "flag: Tristan da Cunha"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa8"), "flag: Turks & Caicos Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa9"), "flag: Chad"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xab"), "flag: French Southern Territories"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xac"), "flag: Togo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xad"), "flag: Thailand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xaf"), "flag: Tajikistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb0"), "flag: Tokelau"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb1"), "flag: Timor-Leste"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb2"), "flag: Turkmenistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb3"), "flag: Tunisia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb4"), "flag: Tonga"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb7"), "flag: Turkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb9"), "flag: Trinidad & Tobago"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbb"), "flag: Tuvalu"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbc"), "flag: Taiwan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbf"), "flag: Tanzania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xa6"), "flag: Ukraine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xac"), "flag: Uganda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb2"), "flag: U.S. Outlying Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb3"), "flag: United Nations"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb8"), "flag: United States"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbe"), "flag: Uruguay"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbf"), "flag: Uzbekistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa6"), "flag: Vatican City"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa8"), "flag: St. Vincent & Grenadines"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xaa"), "flag: Venezuela"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xac"), "flag: British Virgin Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xae"), "flag: U.S. Virgin Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xb3"), "flag: Vietnam"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xba"), "flag: Vanuatu"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xab"), "flag: Wallis & Futuna"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xb8"), "flag: Samoa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbd\xf0\x9f\x87\xb0"), "flag: Kosovo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xaa"), "flag: Yemen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xb9"), "flag: Mayotte"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xa6"), "flag: South Africa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xb2"), "flag: Zambia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xbc"), "flag: Zimbabwe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xa5\xf3\xa0" + "\x81\xae\xf3\xa0\x81\xa7\xf3\xa0\x81\xbf"), + "flag: England"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xb3\xf3\xa0" + "\x81\xa3\xf3\xa0\x81\xb4\xf3\xa0\x81\xbf"), + "flag: Scotland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xb7\xf3\xa0" + "\x81\xac\xf3\xa0\x81\xb3\xf3\xa0\x81\xbf"), + "flag: Wales"}, }; diff --git a/src/main.cpp b/src/main.cpp index 0c196a33..042ef8c0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,16 +15,20 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <iostream> + #include <QApplication> #include <QCommandLineParser> #include <QDesktopWidget> #include <QDir> #include <QFile> #include <QFontDatabase> +#include <QGuiApplication> #include <QLabel> #include <QLibraryInfo> #include <QMessageBox> #include <QPoint> +#include <QScreen> #include <QSettings> #include <QStandardPaths> #include <QTranslator> @@ -33,17 +37,22 @@ #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" -#include "RunGuard.h" #include "Utils.h" #include "config/nheko.h" +#include "singleapplication.h" #if defined(Q_OS_MAC) #include "emoji/MacHelper.h" #endif +#ifdef QML_DEBUGGING +#include <QQmlDebuggingEnabler> +QQmlDebuggingEnabler enabler; +#endif + #if defined(Q_OS_LINUX) #include <boost/stacktrace.hpp> -#include <signal.h> +#include <csignal> void stacktraceHandler(int signum) @@ -72,7 +81,8 @@ registerSignalHandlers() QPoint screenCenter(int width, int height) { - QRect screenGeometry = QApplication::desktop()->screenGeometry(); + // Deprecated in 5.13: QRect screenGeometry = QApplication::desktop()->screenGeometry(); + QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); int x = (screenGeometry.width() - width) / 2; int y = (screenGeometry.height() - height) / 2; @@ -94,18 +104,6 @@ createCacheDirectory() int main(int argc, char *argv[]) { - RunGuard guard("run_guard"); - - if (!guard.tryToRun()) { - QApplication a(argc, argv); - - QMessageBox msgBox; - msgBox.setText("Another instance of Nheko is running"); - msgBox.exec(); - - return 0; - } - #if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD) if (qgetenv("QT_SCALE_FACTOR").size() == 0) { float factor = utils::scaleFactor(); @@ -115,24 +113,23 @@ main(int argc, char *argv[]) } #endif - QApplication app(argc, argv); QCoreApplication::setApplicationName("nheko"); QCoreApplication::setApplicationVersion(nheko::version); QCoreApplication::setOrganizationName("nheko"); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + SingleApplication app(argc, + argv, + false, + SingleApplication::Mode::User | + SingleApplication::Mode::ExcludeAppPath | + SingleApplication::Mode::ExcludeAppVersion); QCommandLineParser parser; parser.addHelpOption(); 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(); @@ -188,6 +185,11 @@ main(int argc, char *argv[]) nhlog::net()->debug("bye"); } }); + QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() { + w.show(); + w.raise(); + w.activateWindow(); + }); #if defined(Q_OS_MAC) // Temporary solution for the emoji picker until diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index d3901c52..1914f61c 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -142,7 +142,11 @@ operator<<(QDBusArgument &arg, const QImage &image) int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); arg << i.depth() / channels; arg << channels; +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.byteCount()); +#else + arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes()); +#endif arg.endStructure(); return arg; } diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp new file mode 100644 index 00000000..db97e4a3 --- /dev/null +++ b/src/popups/PopupItem.cpp @@ -0,0 +1,141 @@ +#include <QPaintEvent> +#include <QPainter> +#include <QStyleOption> + +#include "../Utils.h" +#include "../ui/Avatar.h" +#include "PopupItem.h" + +constexpr int PopupHMargin = 4; +constexpr int PopupItemMargin = 3; + +PopupItem::PopupItem(QWidget *parent) + : QWidget(parent) + , avatar_{new Avatar(this, conf::popup::avatar)} + , hovering_{false} +{ + setMouseTracking(true); + setAttribute(Qt::WA_Hover); + + topLayout_ = new QHBoxLayout(this); + topLayout_->setContentsMargins( + PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin); + + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +void +PopupItem::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + if (underMouse() || hovering_) + p.fillRect(rect(), hoverColor_); +} + +UserItem::UserItem(QWidget *parent) + : PopupItem(parent) +{ + userName_ = new QLabel("Placeholder", this); + avatar_->setLetter("P"); + topLayout_->addWidget(avatar_); + topLayout_->addWidget(userName_, 1); +} + +UserItem::UserItem(QWidget *parent, const QString &user_id) + : PopupItem(parent) + , userId_{user_id} +{ + auto displayName = cache::displayName(ChatPage::instance()->currentRoom(), userId_); + + avatar_->setLetter(utils::firstChar(displayName)); + + // If it's a matrix id we use the second letter. + if (displayName.size() > 1 && displayName.at(0) == '@') + avatar_->setLetter(QChar(displayName.at(1))); + + userName_ = new QLabel(displayName, this); + + topLayout_->addWidget(avatar_); + topLayout_->addWidget(userName_, 1); + + resolveAvatar(user_id); +} + +void +UserItem::updateItem(const QString &user_id) +{ + userId_ = user_id; + + auto displayName = cache::displayName(ChatPage::instance()->currentRoom(), userId_); + + // If it's a matrix id we use the second letter. + if (displayName.size() > 1 && displayName.at(0) == '@') + avatar_->setLetter(QChar(displayName.at(1))); + else + avatar_->setLetter(utils::firstChar(displayName)); + + userName_->setText(displayName); + resolveAvatar(user_id); +} + +void +UserItem::resolveAvatar(const QString &user_id) +{ + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); +} + +void +UserItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() != Qt::RightButton) + emit clicked( + cache::displayName(ChatPage::instance()->currentRoom(), selectedText())); + + QWidget::mousePressEvent(event); +} + +RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) + : PopupItem(parent) + , roomId_{QString::fromStdString(res.room_id)} +{ + auto name = QFontMetrics(QFont()).elidedText( + QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); + + avatar_->setLetter(utils::firstChar(name)); + + roomName_ = new QLabel(name, this); + roomName_->setMargin(0); + + topLayout_->addWidget(avatar_); + topLayout_->addWidget(roomName_, 1); + + avatar_->setImage(QString::fromStdString(res.info.avatar_url)); +} + +void +RoomItem::updateItem(const RoomSearchResult &result) +{ + roomId_ = QString::fromStdString(std::move(result.room_id)); + + auto name = + QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)), + Qt::ElideRight, + parentWidget()->width() - 10); + + roomName_->setText(name); + + avatar_->setImage(QString::fromStdString(result.info.avatar_url)); +} + +void +RoomItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() != Qt::RightButton) + emit clicked(selectedText()); + + QWidget::mousePressEvent(event); +} diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h new file mode 100644 index 00000000..7a710fdb --- /dev/null +++ b/src/popups/PopupItem.h @@ -0,0 +1,83 @@ +#pragma once + +#include <QHBoxLayout> +#include <QLabel> +#include <QPoint> +#include <QWidget> + +#include "../AvatarProvider.h" +#include "../ChatPage.h" + +class Avatar; +struct SearchResult; + +class PopupItem : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor) + Q_PROPERTY(bool hovering READ hovering WRITE setHovering) + +public: + PopupItem(QWidget *parent); + + QString selectedText() const { return QString(); } + QColor hoverColor() const { return hoverColor_; } + void setHoverColor(QColor &color) { hoverColor_ = color; } + + bool hovering() const { return hovering_; } + void setHovering(const bool hover) { hovering_ = hover; }; + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void clicked(const QString &text); + +protected: + QHBoxLayout *topLayout_; + Avatar *avatar_; + QColor hoverColor_; + + //! Set if the item is currently being + //! hovered during tab completion (cycling). + bool hovering_; +}; + +class UserItem : public PopupItem +{ + Q_OBJECT + +public: + UserItem(QWidget *parent); + UserItem(QWidget *parent, const QString &user_id); + QString selectedText() const { return userId_; } + void updateItem(const QString &user_id); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + void resolveAvatar(const QString &user_id); + + QLabel *userName_; + QString userId_; +}; + +class RoomItem : public PopupItem +{ + Q_OBJECT + +public: + RoomItem(QWidget *parent, const RoomSearchResult &res); + QString selectedText() const { return roomId_; } + void updateItem(const RoomSearchResult &res); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + QLabel *roomName_; + QString roomId_; + RoomSearchResult info_; +}; diff --git a/src/popups/ReplyPopup.cpp b/src/popups/ReplyPopup.cpp new file mode 100644 index 00000000..5058c039 --- /dev/null +++ b/src/popups/ReplyPopup.cpp @@ -0,0 +1,103 @@ +#include <QLabel> +#include <QPaintEvent> +#include <QPainter> +#include <QStyleOption> + +#include "../Config.h" +#include "../Utils.h" +#include "../ui/Avatar.h" +#include "../ui/DropShadow.h" +#include "../ui/TextLabel.h" +#include "PopupItem.h" +#include "ReplyPopup.h" + +ReplyPopup::ReplyPopup(QWidget *parent) + : QWidget(parent) + , userItem_{nullptr} + , msgLabel_{nullptr} + , eventLabel_{nullptr} +{ + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); + + mainLayout_ = new QVBoxLayout(this); + mainLayout_->setMargin(0); + mainLayout_->setSpacing(0); + + topLayout_ = new QHBoxLayout(); + topLayout_->setSpacing(0); + topLayout_->setContentsMargins(13, 1, 13, 0); + + userItem_ = new UserItem(this); + connect(userItem_, &UserItem::clicked, this, &ReplyPopup::userSelected); + topLayout_->addWidget(userItem_); + + buttonLayout_ = new QHBoxLayout(); + buttonLayout_->setSpacing(0); + buttonLayout_->setMargin(0); + + topLayout_->addLayout(buttonLayout_); + QFont f; + f.setPointSizeF(f.pointSizeF()); + const int fontHeight = QFontMetrics(f).height(); + buttonSize_ = std::min(fontHeight, 20); + + closeBtn_ = new FlatButton(this); + closeBtn_->setToolTip(tr("Logout")); + closeBtn_->setCornerRadius(buttonSize_ / 4); + closeBtn_->setText("X"); + + QIcon icon; + icon.addFile(":/icons/icons/ui/remove-symbol.png"); + + closeBtn_->setIcon(icon); + closeBtn_->setIconSize(QSize(buttonSize_, buttonSize_)); + connect(closeBtn_, &FlatButton::clicked, this, [this]() { emit cancel(); }); + + buttonLayout_->addWidget(closeBtn_); + + topLayout_->addLayout(buttonLayout_); + + mainLayout_->addLayout(topLayout_); + msgLabel_ = new TextLabel(this); + msgLabel_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); + mainLayout_->addWidget(msgLabel_); + eventLabel_ = new QLabel(this); + mainLayout_->addWidget(eventLabel_); + + setLayout(mainLayout_); +} + +void +ReplyPopup::setReplyContent(const RelatedInfo &related) +{ + // Update the current widget with the new data. + userItem_->updateItem(related.quoted_user); + + msgLabel_->setText(utils::getFormattedQuoteBody(related, "") + .replace("<mx-reply>", "") + .replace("</mx-reply>", "")); + + // eventLabel_->setText(srcEvent); + + adjustSize(); +} + +void +ReplyPopup::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} + +void +ReplyPopup::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() != Qt::RightButton) { + emit clicked(eventLabel_->text()); + } + + QWidget::mousePressEvent(event); +} diff --git a/src/popups/ReplyPopup.h b/src/popups/ReplyPopup.h new file mode 100644 index 00000000..1fa3bb83 --- /dev/null +++ b/src/popups/ReplyPopup.h @@ -0,0 +1,44 @@ +#pragma once + +#include <QHBoxLayout> +#include <QLabel> +#include <QVBoxLayout> +#include <QWidget> + +#include "../ui/FlatButton.h" +#include "../ui/TextLabel.h" + +struct RelatedInfo; +class UserItem; + +class ReplyPopup : public QWidget +{ + Q_OBJECT + +public: + explicit ReplyPopup(QWidget *parent = nullptr); + +public slots: + void setReplyContent(const RelatedInfo &related); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + +signals: + void userSelected(const QString &user); + void clicked(const QString &text); + void cancel(); + +private: + QHBoxLayout *topLayout_; + QVBoxLayout *mainLayout_; + QHBoxLayout *buttonLayout_; + + UserItem *userItem_; + FlatButton *closeBtn_; + TextLabel *msgLabel_; + QLabel *eventLabel_; + + int buttonSize_; +}; diff --git a/src/popups/SuggestionsPopup.cpp b/src/popups/SuggestionsPopup.cpp new file mode 100644 index 00000000..8f355b38 --- /dev/null +++ b/src/popups/SuggestionsPopup.cpp @@ -0,0 +1,156 @@ +#include <QPaintEvent> +#include <QPainter> +#include <QStyleOption> + +#include "../Config.h" +#include "../Utils.h" +#include "../ui/Avatar.h" +#include "../ui/DropShadow.h" +#include "SuggestionsPopup.h" + +SuggestionsPopup::SuggestionsPopup(QWidget *parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); + + layout_ = new QVBoxLayout(this); + layout_->setMargin(0); + layout_->setSpacing(0); +} + +void +SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms) +{ + if (rooms.empty()) { + hide(); + return; + } + + const size_t layoutCount = layout_->count(); + const size_t roomCount = rooms.size(); + + // Remove the extra widgets from the layout. + if (roomCount < layoutCount) + removeLayoutItemsAfter(roomCount - 1); + + for (size_t i = 0; i < roomCount; ++i) { + auto item = layout_->itemAt(i); + + // Create a new widget if there isn't already one in that + // layout position. + if (!item) { + auto room = new RoomItem(this, rooms.at(i)); + connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected); + layout_->addWidget(room); + } else { + // Update the current widget with the new data. + auto room = qobject_cast<RoomItem *>(item->widget()); + if (room) + room->updateItem(rooms.at(i)); + } + } + + resetSelection(); + adjustSize(); + + resize(geometry().width(), 40 * rooms.size()); + + selectNextSuggestion(); +} + +void +SuggestionsPopup::addUsers(const std::vector<SearchResult> &users) +{ + if (users.empty()) { + hide(); + return; + } + + const size_t layoutCount = layout_->count(); + const size_t userCount = users.size(); + + // Remove the extra widgets from the layout. + if (userCount < layoutCount) + removeLayoutItemsAfter(userCount - 1); + + for (size_t i = 0; i < userCount; ++i) { + auto item = layout_->itemAt(i); + + // Create a new widget if there isn't already one in that + // layout position. + if (!item) { + auto user = new UserItem(this, users.at(i).user_id); + connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected); + layout_->addWidget(user); + } else { + // Update the current widget with the new data. + auto userWidget = qobject_cast<UserItem *>(item->widget()); + if (userWidget) + userWidget->updateItem(users.at(i).user_id); + } + } + + resetSelection(); + adjustSize(); + + selectNextSuggestion(); +} + +void +SuggestionsPopup::hoverSelection() +{ + resetHovering(); + setHovering(selectedItem_); + update(); +} + +void +SuggestionsPopup::selectNextSuggestion() +{ + selectedItem_++; + if (selectedItem_ >= layout_->count()) + selectFirstItem(); + + hoverSelection(); +} + +void +SuggestionsPopup::selectPreviousSuggestion() +{ + selectedItem_--; + if (selectedItem_ < 0) + selectLastItem(); + + hoverSelection(); +} + +void +SuggestionsPopup::resetHovering() +{ + for (int i = 0; i < layout_->count(); ++i) { + const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget()); + + if (item) + item->setHovering(false); + } +} + +void +SuggestionsPopup::setHovering(int pos) +{ + const auto &item = layout_->itemAt(pos); + const auto &widget = qobject_cast<PopupItem *>(item->widget()); + + if (widget) + widget->setHovering(true); +} + +void +SuggestionsPopup::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h new file mode 100644 index 00000000..63c44538 --- /dev/null +++ b/src/popups/SuggestionsPopup.h @@ -0,0 +1,75 @@ +#pragma once + +#include <QHBoxLayout> +#include <QLabel> +#include <QPoint> +#include <QWidget> + +#include "CacheStructs.h" +#include "ChatPage.h" +#include "PopupItem.h" + +class SuggestionsPopup : public QWidget +{ + Q_OBJECT + +public: + explicit SuggestionsPopup(QWidget *parent = nullptr); + + template<class Item> + void selectHoveredSuggestion() + { + const auto item = layout_->itemAt(selectedItem_); + if (!item) + return; + + const auto &widget = qobject_cast<Item *>(item->widget()); + emit itemSelected( + cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); + + resetSelection(); + } + +public slots: + void addUsers(const std::vector<SearchResult> &users); + void addRooms(const std::vector<RoomSearchResult> &rooms); + + //! Move to the next available suggestion item. + void selectNextSuggestion(); + //! Move to the previous available suggestion item. + void selectPreviousSuggestion(); + //! Remove hovering from all items. + void resetHovering(); + //! Set hovering to the item in the given layout position. + void setHovering(int pos); + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void itemSelected(const QString &user); + +private: + void hoverSelection(); + void resetSelection() { selectedItem_ = -1; } + void selectFirstItem() { selectedItem_ = 0; } + void selectLastItem() { selectedItem_ = layout_->count() - 1; } + void removeLayoutItemsAfter(size_t startingPos) + { + size_t posToRemove = layout_->count() - 1; + + QLayoutItem *item; + while (startingPos <= posToRemove && + (item = layout_->takeAt(posToRemove)) != nullptr) { + delete item->widget(); + delete item; + + posToRemove = layout_->count() - 1; + } + } + + QVBoxLayout *layout_; + + //! Counter for tab completion (cycling). + int selectedItem_ = -1; +}; diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp new file mode 100644 index 00000000..2e70dbd3 --- /dev/null +++ b/src/popups/UserMentions.cpp @@ -0,0 +1,171 @@ +#include <QPaintEvent> +#include <QPainter> +#include <QScrollArea> +#include <QStyleOption> +#include <QTabWidget> +#include <QTimer> +#include <QVBoxLayout> + +#include "Cache.h" +#include "ChatPage.h" +#include "Logging.h" +#include "UserMentions.h" +//#include "timeline/TimelineItem.h" + +using namespace popups; + +UserMentions::UserMentions(QWidget *parent) + : QWidget{parent} +{ + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::FramelessWindowHint | Qt::Popup); + + tab_layout_ = new QTabWidget(this); + + top_layout_ = new QVBoxLayout(this); + top_layout_->setSpacing(0); + top_layout_->setMargin(0); + + local_scroll_area_ = new QScrollArea(this); + local_scroll_area_->setWidgetResizable(true); + local_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + local_scroll_widget_ = new QWidget(this); + local_scroll_widget_->setObjectName("local_scroll_widget"); + + all_scroll_area_ = new QScrollArea(this); + all_scroll_area_->setWidgetResizable(true); + all_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + all_scroll_widget_ = new QWidget(this); + all_scroll_widget_->setObjectName("all_scroll_widget"); + + // Height of the typing display. + QFont f; + f.setPointSizeF(f.pointSizeF() * 0.9); + const int bottomMargin = QFontMetrics(f).height() + 6; + + local_scroll_layout_ = new QVBoxLayout(local_scroll_widget_); + local_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); + local_scroll_layout_->setSpacing(0); + local_scroll_layout_->setObjectName("localscrollarea"); + + all_scroll_layout_ = new QVBoxLayout(all_scroll_widget_); + all_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); + all_scroll_layout_->setSpacing(0); + all_scroll_layout_->setObjectName("allscrollarea"); + + local_scroll_area_->setWidget(local_scroll_widget_); + local_scroll_area_->setAlignment(Qt::AlignBottom); + + all_scroll_area_->setWidget(all_scroll_widget_); + all_scroll_area_->setAlignment(Qt::AlignBottom); + + tab_layout_->addTab(local_scroll_area_, tr("This Room")); + tab_layout_->addTab(all_scroll_area_, tr("All Rooms")); + top_layout_->addWidget(tab_layout_); + + setLayout(top_layout_); +} + +void +UserMentions::initializeMentions(const QMap<QString, mtx::responses::Notifications> ¬ifs) +{ + nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications."); + + for (const auto &item : notifs) { + for (const auto ¬if : item.notifications) { + const auto event_id = QString::fromStdString(utils::event_id(notif.event)); + + try { + const auto room_id = QString::fromStdString(notif.room_id); + const auto user_id = utils::event_sender(notif.event); + const auto body = utils::event_body(notif.event); + + pushItem(event_id, + user_id, + body, + room_id, + ChatPage::instance()->currentRoom()); + + } catch (const lmdb::error &e) { + nhlog::db()->warn("error while sending desktop notification: {}", + e.what()); + } + } + } +} + +void +UserMentions::showPopup() +{ + for (auto widget : all_scroll_layout_->findChildren<QWidget *>()) { + delete widget; + } + for (auto widget : local_scroll_layout_->findChildren<QWidget *>()) { + delete widget; + } + + auto notifs = cache::getTimelineMentions(); + + initializeMentions(notifs); + show(); +} + +void +UserMentions::pushItem(const QString &event_id, + const QString &user_id, + const QString &body, + const QString &room_id, + const QString ¤t_room_id) +{ + (void)event_id; + (void)user_id; + (void)body; + (void)room_id; + (void)current_room_id; + // setUpdatesEnabled(false); + // + // // Add to the 'all' section + // TimelineItem *view_item = new TimelineItem( + // mtx::events::MessageType::Text, user_id, body, true, room_id, + // all_scroll_widget_); + // view_item->setEventId(event_id); + // view_item->hide(); + // + // all_scroll_layout_->addWidget(view_item); + // QTimer::singleShot(0, this, [view_item, this]() { + // view_item->show(); + // view_item->adjustSize(); + // setUpdatesEnabled(true); + // }); + // + // // if it matches the current room... add it to the current room as well. + // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { + // // Add to the 'local' section + // TimelineItem *local_view_item = new + // TimelineItem(mtx::events::MessageType::Text, + // user_id, + // body, + // true, + // room_id, + // local_scroll_widget_); + // local_view_item->setEventId(event_id); + // local_view_item->hide(); + // local_scroll_layout_->addWidget(local_view_item); + // + // QTimer::singleShot(0, this, [local_view_item]() { + // local_view_item->show(); + // local_view_item->adjustSize(); + // }); + // } +} + +void +UserMentions::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h new file mode 100644 index 00000000..b7c4e51d --- /dev/null +++ b/src/popups/UserMentions.h @@ -0,0 +1,45 @@ +#pragma once + +#include <mtx/responses.hpp> + +#include <QMap> +#include <QString> +#include <QWidget> + +class QPaintEvent; +class QTabWidget; +class QScrollArea; +class QVBoxLayout; + +namespace popups { + +class UserMentions : public QWidget +{ + Q_OBJECT +public: + UserMentions(QWidget *parent = nullptr); + + void initializeMentions(const QMap<QString, mtx::responses::Notifications> ¬ifs); + void showPopup(); + +protected: + void paintEvent(QPaintEvent *) override; + +private: + void pushItem(const QString &event_id, + const QString &user_id, + const QString &body, + const QString &room_id, + const QString ¤t_room_id); + QTabWidget *tab_layout_; + QVBoxLayout *top_layout_; + QVBoxLayout *local_scroll_layout_; + QVBoxLayout *all_scroll_layout_; + + QScrollArea *local_scroll_area_; + QWidget *local_scroll_widget_; + + QScrollArea *all_scroll_area_; + QWidget *all_scroll_widget_; +}; +} diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp new file mode 100644 index 00000000..46ab6c0e --- /dev/null +++ b/src/timeline/DelegateChooser.cpp @@ -0,0 +1,138 @@ +#include "DelegateChooser.h" + +#include "Logging.h" + +// uses private API, which moved between versions +#include <QQmlEngine> +#include <QtGlobal> + +QQmlComponent * +DelegateChoice::delegate() const +{ + return delegate_; +} + +void +DelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QVariant +DelegateChoice::roleValue() const +{ + return roleValue_; +} + +void +DelegateChoice::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + emit roleValueChanged(); + emit changed(); + } +} + +QVariant +DelegateChooser::roleValue() const +{ + return roleValue_; +} + +void +DelegateChooser::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + recalcChild(); + emit roleValueChanged(); + } +} + +QQmlListProperty<DelegateChoice> +DelegateChooser::choices() +{ + return QQmlListProperty<DelegateChoice>(this, + this, + &DelegateChooser::appendChoice, + &DelegateChooser::choiceCount, + &DelegateChooser::choice, + &DelegateChooser::clearChoices); +} + +void +DelegateChooser::appendChoice(QQmlListProperty<DelegateChoice> *p, DelegateChoice *c) +{ + DelegateChooser *dc = static_cast<DelegateChooser *>(p->object); + dc->choices_.append(c); +} + +int +DelegateChooser::choiceCount(QQmlListProperty<DelegateChoice> *p) +{ + return static_cast<DelegateChooser *>(p->object)->choices_.count(); +} +DelegateChoice * +DelegateChooser::choice(QQmlListProperty<DelegateChoice> *p, int index) +{ + return static_cast<DelegateChooser *>(p->object)->choices_.at(index); +} +void +DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p) +{ + static_cast<DelegateChooser *>(p->object)->choices_.clear(); +} + +void +DelegateChooser::recalcChild() +{ + for (const auto choice : qAsConst(choices_)) { + auto choiceValue = choice->roleValue(); + if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { + if (child) { + child->setParentItem(nullptr); + child = nullptr; + } + + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); + return; + } + } +} + +void +DelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + recalcChild(); +} + +void +DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + chooser.child = dynamic_cast<QQuickItem *>(object()); + if (chooser.child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + chooser.child->setParentItem(&chooser); + connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { + chooser.setHeight(chooser.child->height()); + }); + chooser.setHeight(chooser.child->height()); + QQmlEngine::setObjectOwnership(chooser.child, + QQmlEngine::ObjectOwnership::JavaScriptOwnership); + + } else if (status == QQmlIncubator::Error) { + for (const auto &e : errors()) + nhlog::ui()->error("Error instantiating delegate: {}", + e.toString().toStdString()); + } +} diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h new file mode 100644 index 00000000..68ebeb04 --- /dev/null +++ b/src/timeline/DelegateChooser.h @@ -0,0 +1,82 @@ +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt +// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent + +#pragma once + +#include <QQmlComponent> +#include <QQmlIncubator> +#include <QQmlListProperty> +#include <QQuickItem> +#include <QtCore/QObject> +#include <QtCore/QVariant> + +class QQmlAdaptorModel; + +class DelegateChoice : public QObject +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + +signals: + void delegateChanged(); + void roleValueChanged(); + void changed(); + +private: + QVariant roleValue_; + QQmlComponent *delegate_ = nullptr; +}; + +class DelegateChooser : public QQuickItem +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") + +public: + Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + + QQmlListProperty<DelegateChoice> choices(); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + + void recalcChild(); + void componentComplete() override; + +signals: + void roleChanged(); + void roleValueChanged(); + +private: + struct DelegateIncubator : public QQmlIncubator + { + DelegateIncubator(DelegateChooser &parent) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + {} + void statusChanged(QQmlIncubator::Status status) override; + + DelegateChooser &chooser; + }; + + QVariant roleValue_; + QList<DelegateChoice *> choices_; + QQuickItem *child = nullptr; + DelegateIncubator incubator{*this}; + + static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *); + static int choiceCount(QQmlListProperty<DelegateChoice> *); + static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index); + static void clearChoices(QQmlListProperty<DelegateChoice> *); +}; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp deleted file mode 100644 index d23dbf49..00000000 --- a/src/timeline/TimelineItem.cpp +++ /dev/null @@ -1,918 +0,0 @@ -/* - * 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 <functional> - -#include <QContextMenuEvent> -#include <QDesktopServices> -#include <QFontDatabase> -#include <QMenu> -#include <QTimer> - -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "ui/Avatar.h" -#include "ui/Painter.h" -#include "ui/TextLabel.h" - -#include "timeline/TimelineItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -#include "dialogs/RawMessage.h" -#include "mtx/identifiers.hpp" - -constexpr int MSG_RIGHT_MARGIN = 7; -constexpr int MSG_PADDING = 20; - -StatusIndicator::StatusIndicator(QWidget *parent) - : QWidget(parent) -{ - lockIcon_.addFile(":/icons/icons/ui/lock.png"); - clockIcon_.addFile(":/icons/icons/ui/clock.png"); - checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); - doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png"); -} - -void -StatusIndicator::paintIcon(QPainter &p, QIcon &icon) -{ - auto pixmap = icon.pixmap(width()); - - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(pixmap.rect(), p.pen().color()); - - QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal); -} - -void -StatusIndicator::paintEvent(QPaintEvent *) -{ - if (state_ == StatusIndicatorState::Empty) - return; - - Painter p(this); - PainterHighQualityEnabler hq(p); - - p.setPen(iconColor_); - - switch (state_) { - case StatusIndicatorState::Sent: { - paintIcon(p, clockIcon_); - break; - } - case StatusIndicatorState::Encrypted: - paintIcon(p, lockIcon_); - break; - case StatusIndicatorState::Received: { - paintIcon(p, checkmarkIcon_); - break; - } - case StatusIndicatorState::Read: { - paintIcon(p, doubleCheckmarkIcon_); - break; - } - case StatusIndicatorState::Empty: - break; - } -} - -void -StatusIndicator::setState(StatusIndicatorState state) -{ - state_ = state; - - switch (state) { - case StatusIndicatorState::Encrypted: - setToolTip(tr("Encrypted")); - break; - case StatusIndicatorState::Received: - setToolTip(tr("Delivered")); - break; - case StatusIndicatorState::Read: - setToolTip(tr("Seen")); - break; - case StatusIndicatorState::Sent: - setToolTip(tr("Sent")); - break; - case StatusIndicatorState::Empty: - setToolTip(""); - break; - } - - update(); -} - -void -TimelineItem::adjustMessageLayoutForWidget() -{ - messageLayout_->addLayout(widgetLayout_, 1); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::adjustMessageLayout() -{ - messageLayout_->addWidget(body_, 1); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::init() -{ - userAvatar_ = nullptr; - timestamp_ = nullptr; - userName_ = nullptr; - body_ = nullptr; - - contextMenu_ = new QMenu(this); - showReadReceipts_ = new QAction("Read receipts", this); - markAsRead_ = new QAction("Mark as read", this); - viewRawMessage_ = new QAction("View raw message", this); - redactMsg_ = new QAction("Redact message", this); - contextMenu_->addAction(showReadReceipts_); - contextMenu_->addAction(viewRawMessage_); - contextMenu_->addAction(markAsRead_); - contextMenu_->addAction(redactMsg_); - - connect(showReadReceipts_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - MainWindow::instance()->openReadReceiptsDialog(event_id_); - }); - - connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) { - emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id); - }); - connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); - connect(redactMsg_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - event_id_.toStdString(), - [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed(tr("Message redaction failed: %1") - .arg(QString::fromStdString( - err->matrix_error.error))); - return; - } - - 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; - messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0); - messageLayout_->setSpacing(MSG_PADDING); - - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0); - topLayout_->setSpacing(0); - topLayout_->addLayout(mainLayout_); - - mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); - mainLayout_->setSpacing(0); - - timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9); - timestampFont_.setFamily("Monospace"); - timestampFont_.setStyleHint(QFont::Monospace); - - QFontMetrics tsFm(timestampFont_); - - statusIndicator_ = new StatusIndicator(this); - statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading()); - statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading()); - - parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); -} - -/* - * For messages created locally. - */ -TimelineItem::TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - // Generate the html body to be rendered. - auto formatted_body = utils::markdownToHtml(body); - - // Escape html if the input is not formatted. - if (formatted_body == body.trimmed().toHtmlEscaped()) - formatted_body = body.toHtmlEscaped(); - - QString emptyEventId; - - if (ty == mtx::events::MessageType::Emote) { - formatted_body = QString("<em>%1</em>").arg(formatted_body); - descriptionMsg_ = {emptyEventId, - "", - userid, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - } else { - descriptionMsg_ = {emptyEventId, - "You: ", - userid, - body, - utils::descriptiveTime(timestamp), - timestamp}; - } - - formatted_body = utils::linkifyMessage(formatted_body); - - generateTimestamp(timestamp); - - if (withSender) { - generateBody(userid, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::TimelineItem(ImageItem *image, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<ImageItem>(image, userid, withSender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<FileItem>(file, userid, withSender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<AudioItem>(audio, userid, withSender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<VideoItem>(video, userid, withSender); -} - -TimelineItem::TimelineItem(ImageItem *image, - const mtx::events::RoomEvent<mtx::events::msg::Image> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>( - image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(StickerItem *image, - const mtx::events::Sticker &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const mtx::events::RoomEvent<mtx::events::msg::File> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>( - file, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>( - audio, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const mtx::events::RoomEvent<mtx::events::msg::Video> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>( - video, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -/* - * Used to display remote notice messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - descriptionMsg_ = {event_id_, - Cache::displayName(room_id_, sender), - sender, - " sent a notification", - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - auto displayName = Cache::displayName(room_id_, sender); - - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote emote messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - formatted_body = QString("<em>%1</em>").arg(formatted_body); - - descriptionMsg_ = {event_id_, - "", - sender, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote text messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(": %1").arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::~TimelineItem() -{ - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); -} - -void -TimelineItem::markSent() -{ - statusIndicator_->setState(StatusIndicatorState::Sent); -} - -void -TimelineItem::markOwnMessagesAsReceived(const std::string &sender) -{ - QSettings settings; - if (sender == settings.value("auth/user_id").toString().toStdString()) - statusIndicator_->setState(StatusIndicatorState::Received); -} - -void -TimelineItem::markRead() -{ - if (statusIndicator_->state() != StatusIndicatorState::Encrypted) - statusIndicator_->setState(StatusIndicatorState::Read); -} - -void -TimelineItem::markReceived(bool isEncrypted) -{ - isReceived_ = true; - - if (isEncrypted) - statusIndicator_->setState(StatusIndicatorState::Encrypted); - else - statusIndicator_->setState(StatusIndicatorState::Received); - - sendReadReceipt(); -} - -// Only the body is displayed. -void -TimelineItem::generateBody(const QString &body) -{ - body_ = new TextLabel(replaceEmoji(body), this); - body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - - connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { - MainWindow::instance()->openUserProfile(user_id, - ChatPage::instance()->currentRoom()); - }); -} - -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) -{ - generateUserName(user_id, displayname); - generateBody(body); -} - -void -TimelineItem::generateUserName(const QString &user_id, const QString &displayname) -{ - auto sender = displayname; - - if (displayname.startsWith("@")) { - // TODO: Fix this by using a UserId type. - if (displayname.split(":")[0].split("@").size() > 1) - sender = displayname.split(":")[0].split("@")[1]; - } - - QFont usernameFont; - usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); - usernameFont.setWeight(QFont::Medium); - - QFontMetrics fm(usernameFont); - - userName_ = new QLabel(this); - userName_->setFont(usernameFont); - userName_->setText(fm.elidedText(sender, Qt::ElideRight, 500)); - userName_->setToolTip(user_id); - userName_->setToolTipDuration(1500); - userName_->setAttribute(Qt::WA_Hover); - 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); - - connect(filter, &UserProfileFilter::hoverOn, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(true); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::hoverOff, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(false); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() { - MainWindow::instance()->openUserProfile(user_id, room_id_); - }); -} - -void -TimelineItem::generateTimestamp(const QDateTime &time) -{ - timestamp_ = new QLabel(this); - timestamp_->setFont(timestampFont_); - timestamp_->setText( - 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) -{ - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0); - - QFont f; - f.setPointSizeF(f.pointSizeF()); - - userAvatar_ = new Avatar(this); - userAvatar_->setLetter(QChar(userName[0]).toUpper()); - userAvatar_->setSize(QFontMetrics(f).height() * 2); - - // TODO: The provided user name should be a UserId class - if (userName[0] == '@' && userName.size() > 1) - userAvatar_->setLetter(QChar(userName[1]).toUpper()); - - topLayout_->insertWidget(0, userAvatar_); - topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft); - - if (userName_) - mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft); -} - -void -TimelineItem::setupSimpleLayout() -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - topLayout_->setContentsMargins(conf::timeline::msgLeftMargin + - QFontMetrics(f).height() * 2 + 2, - conf::timeline::msgTopMargin, - 0, - 0); -} - -void -TimelineItem::setUserAvatar(const QImage &avatar) -{ - if (userAvatar_ == nullptr) - return; - - userAvatar_->setImage(avatar); -} - -void -TimelineItem::contextMenuEvent(QContextMenuEvent *event) -{ - if (contextMenu_) - contextMenu_->exec(event->globalPos()); -} - -void -TimelineItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineItem::addSaveImageAction(ImageItem *image) -{ - if (contextMenu_) { - auto saveImage = new QAction("Save image", this); - contextMenu_->addAction(saveImage); - - connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs); - } -} - -void -TimelineItem::addReplyAction() -{ - if (contextMenu_) { - auto replyAction = new QAction("Reply", this); - contextMenu_->addAction(replyAction); - - connect(replyAction, &QAction::triggered, this, [this]() { - if (!body_) - return; - - emit ChatPage::instance()->messageReply( - Cache::displayName(room_id_, descriptionMsg_.userid), - body_->toPlainText()); - }); - } -} - -void -TimelineItem::addKeyRequestAction() -{ - if (contextMenu_) { - auto requestKeys = new QAction("Request encryption keys", this); - contextMenu_->addAction(requestKeys); - - connect(requestKeys, &QAction::triggered, this, [this]() { - olm::request_keys(room_id_.toStdString(), event_id_.toStdString()); - }); - } -} - -void -TimelineItem::addAvatar() -{ - if (userAvatar_) - return; - - // TODO: should be replaced with the proper event struct. - auto userid = descriptionMsg_.userid; - auto displayName = Cache::displayName(room_id_, userid); - - generateUserName(userid, displayName); - - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); -} - -void -TimelineItem::sendReadReceipt() const -{ - if (!event_id_.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - event_id_.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", - room_id_.toStdString(), - event_id_.toStdString()); - } - }); -} - -void -TimelineItem::openRawMessageViewer() const -{ - const auto event_id = event_id_.toStdString(); - const auto room_id = room_id_.toStdString(); - - auto proxy = std::make_shared<EventProxy>(); - connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) { - auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))}; - Q_UNUSED(dialog); - }); - - http::client()->get_event( - room_id, - event_id, - [event_id, room_id, proxy = std::move(proxy)]( - const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) { - using namespace mtx::events; - - if (err) { - nhlog::net()->warn( - "failed to retrieve event {} from {}", event_id, room_id); - return; - } - - try { - emit proxy->eventRetrieved(utils::serialize_event(res)); - } catch (const nlohmann::json::exception &e) { - nhlog::net()->warn( - "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 deleted file mode 100644 index 7bf6a076..00000000 --- a/src/timeline/TimelineItem.h +++ /dev/null @@ -1,377 +0,0 @@ -/* - * 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 <QApplication> -#include <QDateTime> -#include <QHBoxLayout> -#include <QLabel> -#include <QLayout> -#include <QPainter> -#include <QSettings> -#include <QTimer> - -#include <QtConcurrent> - -#include "AvatarProvider.h" -#include "RoomInfoListItem.h" -#include "Utils.h" - -#include "Cache.h" -#include "MatrixClient.h" - -class ImageItem; -class StickerItem; -class AudioItem; -class VideoItem; -class FileItem; -class Avatar; -class TextLabel; - -enum class StatusIndicatorState -{ - //! The encrypted message was received by the server. - Encrypted, - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, -}; - -//! -//! Used to notify the user about the status of a message. -//! -class StatusIndicator : public QWidget -{ - Q_OBJECT - -public: - explicit StatusIndicator(QWidget *parent); - void setState(StatusIndicatorState state); - StatusIndicatorState state() const { return state_; } - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void paintIcon(QPainter &p, QIcon &icon); - - QIcon lockIcon_; - QIcon clockIcon_; - QIcon checkmarkIcon_; - QIcon doubleCheckmarkIcon_; - - QColor iconColor_ = QColor("#999"); - - StatusIndicatorState state_ = StatusIndicatorState::Empty; - - static constexpr int MaxWidth = 24; -}; - -class EventProxy : public QObject -{ - Q_OBJECT - -signals: - void eventRetrieved(const nlohmann::json &); -}; - -class UserProfileFilter : public QObject -{ - Q_OBJECT - -public: - explicit UserProfileFilter(const QString &user_id, QLabel *parent) - : QObject(parent) - , user_id_{user_id} - {} - -signals: - void hoverOff(); - void hoverOn(); - void clicked(); - -protected: - bool eventFilter(QObject *obj, QEvent *event) - { - if (event->type() == QEvent::MouseButtonRelease) { - emit clicked(); - return true; - } else if (event->type() == QEvent::HoverLeave) { - emit hoverOff(); - return true; - } else if (event->type() == QEvent::HoverEnter) { - emit hoverOn(); - return true; - } - - return QObject::eventFilter(obj, event); - } - -private: - QString user_id_; -}; - -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, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - - // For local messages. - // m.text & m.emote - TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - // m.image - TimelineItem(ImageItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(FileItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(AudioItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(VideoItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - - TimelineItem(ImageItem *img, - const mtx::events::RoomEvent<mtx::events::msg::Image> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(StickerItem *img, - const mtx::events::Sticker &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(FileItem *file, - const mtx::events::RoomEvent<mtx::events::msg::File> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent<mtx::events::msg::Audio> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(VideoItem *video, - const mtx::events::RoomEvent<mtx::events::msg::Video> &e, - bool with_sender, - 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_; } - void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(bool isEncrypted); - void markRead(); - void markSent(); - bool isReceived() { return isReceived_; }; - void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const; - void openRawMessageViewer() const; - - //! Add a user avatar for this event. - void addAvatar(); - void addKeyRequestAction(); - -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; - -private: - //! If we are the sender of the message the event wil be marked as received by the server. - void markOwnMessagesAsReceived(const std::string &sender); - void init(); - //! Add a context menu option to save the image of the timeline item. - void addSaveImageAction(ImageItem *image); - //! Add the reply action in the context menu for widgets that support it. - void addReplyAction(); - - template<class Widget> - void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender); - - template<class Event, class Widget> - void setupWidgetLayout(Widget *widget, const Event &event, bool withSender); - - void generateBody(const QString &body); - void generateBody(const QString &user_id, const QString &displayname, const QString &body); - void generateTimestamp(const QDateTime &time); - void generateUserName(const QString &userid, const QString &displayname); - - void setupAvatarLayout(const QString &userName); - void setupSimpleLayout(); - - void adjustMessageLayout(); - void adjustMessageLayoutForWidget(); - - //! Whether or not the event associated with the widget - //! has been acknowledged by the server. - bool isReceived_ = false; - - QFutureWatcher<QString> *colorGenerating_; - - QString replaceEmoji(const QString &body); - QString event_id_; - QString room_id_; - - DescInfo descriptionMsg_; - - QMenu *contextMenu_; - QAction *showReadReceipts_; - QAction *markAsRead_; - QAction *redactMsg_; - QAction *viewRawMessage_; - QAction *replyMsg_; - - QHBoxLayout *topLayout_ = nullptr; - QHBoxLayout *messageLayout_ = nullptr; - QVBoxLayout *mainLayout_ = nullptr; - QHBoxLayout *widgetLayout_ = nullptr; - - Avatar *userAvatar_; - - QFont timestampFont_; - - StatusIndicator *statusIndicator_; - - QLabel *timestamp_; - QLabel *userName_; - TextLabel *body_; - - QColor backgroundColor_; -}; - -template<class Widget> -void -TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender) -{ - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"", // No event_id up until this point. - "You", - userid, - QString(" %1").arg(utils::messageDescription<Widget>()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout; - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(userid, displayName, ""); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} - -template<class Event, class Widget> -void -TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) -{ - init(); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(" %1").arg(utils::messageDescription<Widget>()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(sender, displayName, ""); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp new file mode 100644 index 00000000..cad39bc5 --- /dev/null +++ b/src/timeline/TimelineModel.cpp @@ -0,0 +1,1570 @@ +#include "TimelineModel.h" + +#include <algorithm> +#include <thread> +#include <type_traits> + +#include <QFileDialog> +#include <QMimeDatabase> +#include <QRegularExpression> +#include <QStandardPaths> + +#include "ChatPage.h" +#include "EventAccessors.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "Olm.h" +#include "TimelineViewManager.h" +#include "Utils.h" +#include "dialogs/RawMessage.h" + +Q_DECLARE_METATYPE(QModelIndex) + +namespace { +struct RoomEventType +{ + template<class T> + qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e) + { + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::Create; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::GuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::HistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::JoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + return qml_mtx_events::EventType::Unsupported; + default: + return qml_mtx_events::EventType::UnknownMessage; + } + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &) + { + return qml_mtx_events::EventType::AudioMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &) + { + return qml_mtx_events::EventType::EmoteMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &) + { + return qml_mtx_events::EventType::FileMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &) + { + return qml_mtx_events::EventType::ImageMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &) + { + return qml_mtx_events::EventType::NoticeMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &) + { + return qml_mtx_events::EventType::TextMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &) + { + return qml_mtx_events::EventType::VideoMessage; + } + + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &) + { + return qml_mtx_events::EventType::Redacted; + } + // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return + // ::EventType::LocationMessage; } +}; +} + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(RoomEventType{}, event); +} + +QString +toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); }, + event); +} + +TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) + , manager_(manager) +{ + connect( + this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.removeOne(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + isProcessingPending = false; + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + pending.removeOne(txn_id); + + int idx = idToIndex(txn_id); + if (idx < 0) { + // transaction already received via sync + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = std::visit( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + + events.remove(txn_id); + events.insert(event_id, ev); + + // mark our messages as read + readEvent(event_id.toStdString()); + + // ask to be notified for read receipts + cache::addPendingReceipt(room_id_, event_id); + + isProcessingPending = false; + emit dataChanged(index(idx, 0), index(idx, 0)); + + if (pending.size() > 0) + emit nextPendingMessage(); + }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); + + connect( + this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage); + connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage); + + connect(this, + &TimelineModel::eventFetched, + this, + [this](QString requestingEvent, mtx::events::collections::TimelineEvents event) { + events.insert(QString::fromStdString(mtx::accessors::event_id(event)), + event); + auto idx = idToIndex(requestingEvent); + if (idx >= 0) + emit dataChanged(index(idx, 0), index(idx, 0)); + }); +} + +QHash<int, QByteArray> +TimelineModel::roleNames() const +{ + return { + {Section, "section"}, + {Type, "type"}, + {TypeString, "typeString"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, + {State, "state"}, + {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, + {RoomName, "roomName"}, + {RoomTopic, "roomTopic"}, + {Dump, "dump"}, + }; +} +int +TimelineModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return (int)this->eventOrder.size(); +} + +QVariantMap +TimelineModel::getDump(QString eventId) const +{ + if (events.contains(eventId)) + return data(eventId, Dump).toMap(); + return {}; +} + +QVariant +TimelineModel::data(const QString &id, int role) const +{ + using namespace mtx::accessors; + namespace acc = mtx::accessors; + mtx::events::collections::TimelineEvents event = events.value(id); + + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + switch (role) { + case UserId: + return QVariant(QString::fromStdString(acc::sender(event))); + case UserName: + return QVariant(displayName(QString::fromStdString(acc::sender(event)))); + + case Timestamp: + return QVariant(origin_server_ts(event)); + case Type: + return QVariant(toRoomEventType(event)); + case TypeString: + return QVariant(toRoomEventTypeString(event)); + case Body: + return QVariant(utils::replaceEmoji(QString::fromStdString(body(event)))); + case FormattedBody: { + const static QRegularExpression replyFallback( + "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption); + + bool isReply = !in_reply_to_event(event).empty(); + + auto formattedBody_ = QString::fromStdString(formatted_body(event)); + if (formattedBody_.isEmpty()) { + auto body_ = QString::fromStdString(body(event)); + + if (isReply) { + while (body_.startsWith("> ")) + body_ = body_.right(body_.size() - body_.indexOf('\n') - 1); + if (body_.startsWith('\n')) + body_ = body_.right(body_.size() - 1); + } + formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>"); + } else { + if (isReply) + formattedBody_ = formattedBody_.remove(replyFallback); + } + return QVariant(utils::replaceEmoji( + utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_)))); + } + case Url: + return QVariant(QString::fromStdString(url(event))); + case ThumbnailUrl: + return QVariant(QString::fromStdString(thumbnail_url(event))); + case Filename: + return QVariant(QString::fromStdString(filename(event))); + case Filesize: + return QVariant(utils::humanReadableFileSize(filesize(event))); + case MimeType: + return QVariant(QString::fromStdString(mimetype(event))); + case Height: + return QVariant(qulonglong{media_height(event)}); + case Width: + return QVariant(qulonglong{media_width(event)}); + case ProportionalHeight: { + auto w = media_width(event); + if (w == 0) + w = 1; + + double prop = media_height(event) / (double)w; + + return QVariant(prop > 0 ? prop : 1.); + } + case Id: + return id; + case State: + // only show read receipts for messages not from us + if (acc::sender(event) != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else if (read.contains(id) || cache::readReceipts(id, room_id_).size() > 1) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + case IsEncrypted: { + return std::holds_alternative< + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]); + } + case ReplyTo: + return QVariant(QString::fromStdString(in_reply_to_event(event))); + case RoomName: + return QVariant(QString::fromStdString(room_name(event))); + case RoomTopic: + return QVariant(QString::fromStdString(room_topic(event))); + case Dump: { + QVariantMap m; + auto names = roleNames(); + + // m.insert(names[Section], data(id, static_cast<int>(Section))); + m.insert(names[Type], data(id, static_cast<int>(Type))); + m.insert(names[TypeString], data(id, static_cast<int>(TypeString))); + m.insert(names[Body], data(id, static_cast<int>(Body))); + m.insert(names[FormattedBody], data(id, static_cast<int>(FormattedBody))); + m.insert(names[UserId], data(id, static_cast<int>(UserId))); + m.insert(names[UserName], data(id, static_cast<int>(UserName))); + m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp))); + m.insert(names[Url], data(id, static_cast<int>(Url))); + m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl))); + m.insert(names[Filename], data(id, static_cast<int>(Filename))); + m.insert(names[Filesize], data(id, static_cast<int>(Filesize))); + m.insert(names[MimeType], data(id, static_cast<int>(MimeType))); + m.insert(names[Height], data(id, static_cast<int>(Height))); + m.insert(names[Width], data(id, static_cast<int>(Width))); + m.insert(names[ProportionalHeight], data(id, static_cast<int>(ProportionalHeight))); + m.insert(names[Id], data(id, static_cast<int>(Id))); + m.insert(names[State], data(id, static_cast<int>(State))); + m.insert(names[IsEncrypted], data(id, static_cast<int>(IsEncrypted))); + m.insert(names[ReplyTo], data(id, static_cast<int>(ReplyTo))); + m.insert(names[RoomName], data(id, static_cast<int>(RoomName))); + m.insert(names[RoomTopic], data(id, static_cast<int>(RoomTopic))); + + return QVariant(m); + } + default: + return QVariant(); + } +} + +QVariant +TimelineModel::data(const QModelIndex &index, int role) const +{ + using namespace mtx::accessors; + namespace acc = mtx::accessors; + if (index.row() < 0 && index.row() >= (int)eventOrder.size()) + return QVariant(); + + QString id = eventOrder[index.row()]; + + mtx::events::collections::TimelineEvents event = events.value(id); + + if (role == Section) { + QDateTime date = origin_server_ts(event); + date.setTime(QTime()); + + std::string userId = acc::sender(event); + + for (size_t r = index.row() + 1; r < eventOrder.size(); r++) { + auto tempEv = events.value(eventOrder[r]); + QDateTime prevDate = origin_server_ts(tempEv); + prevDate.setTime(QTime()); + if (prevDate != date) + return QString("%2 %1") + .arg(date.toMSecsSinceEpoch()) + .arg(QString::fromStdString(userId)); + + std::string prevUserId = acc::sender(tempEv); + if (userId != prevUserId) + break; + } + + return QString("%1").arg(QString::fromStdString(userId)); + } + + return data(id, role); +} + +bool +TimelineModel::canFetchMore(const QModelIndex &) const +{ + if (eventOrder.empty()) + return true; + if (!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>( + events[eventOrder.back()])) + return true; + else + + return false; +} + +void +TimelineModel::fetchMore(const QModelIndex &) +{ + if (paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } + + paginationInProgress = true; + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + nhlog::ui()->debug("Paginating room {}", opts.room_id); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + paginationInProgress = false; + return; + } + + emit oldMessagesRetrieved(std::move(res)); + paginationInProgress = false; + }); +} + +void +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) +{ + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); + isInitialSync = false; + } + + if (timeline.events.empty()) + return; + + std::vector<QString> ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + + updateLastMessage(); +} + +template<typename T> +auto +isMessage(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool> +{ + return true; +} + +template<typename T> +auto +isMessage(const mtx::events::Event<T> &) +{ + return false; +} + +void +TimelineModel::updateLastMessage() +{ + for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) { + auto event = events.value(*it); + if (auto e = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &event)) { + event = decryptEvent(*e).event; + } + + if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, event)) + continue; + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); + return; + } +} + +std::vector<QString> +TimelineModel::internalAddEvents( + const std::vector<mtx::events::collections::TimelineEvents> &timeline) +{ + std::vector<QString> ids; + for (auto e : timeline) { + QString id = QString::fromStdString(mtx::accessors::event_id(e)); + + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + QString txid = QString::fromStdString(mtx::accessors::transaction_id(e)); + if (this->pending.removeOne(txid)) { + this->events.insert(id, e); + this->events.remove(txid); + int idx = idToIndex(txid); + if (idx < 0) { + nhlog::ui()->warn("Received index out of range"); + continue; + } + eventOrder[idx] = id; + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + if (auto redaction = + std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) { + QString redacts = QString::fromStdString(redaction->redacts); + auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); + + if (redacted != eventOrder.end()) { + auto redactedEvent = std::visit( + [](const auto &ev) + -> mtx::events::RoomEvent<mtx::events::msg::Redacted> { + mtx::events::RoomEvent<mtx::events::msg::Redacted> + replacement = {}; + replacement.event_id = ev.event_id; + replacement.room_id = ev.room_id; + replacement.sender = ev.sender; + replacement.origin_server_ts = ev.origin_server_ts; + replacement.type = ev.type; + return replacement; + }, + e); + events.insert(redacts, redactedEvent); + + int row = (int)std::distance(eventOrder.begin(), redacted); + emit dataChanged(index(row, 0), index(row, 0)); + } + + continue; // don't insert redaction into timeline + } + + if (auto event = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) { + e = decryptEvent(*event).event; + } + auto encInfo = mtx::accessors::file(e); + + if (encInfo) + emit newEncryptedImage(encInfo.value()); + + this->events.insert(id, e); + ids.push_back(id); + + auto replyTo = mtx::accessors::in_reply_to_event(e); + auto qReplyTo = QString::fromStdString(replyTo); + if (!replyTo.empty() && !events.contains(qReplyTo)) { + http::client()->get_event( + this->room_id_.toStdString(), + replyTo, + [this, id, replyTo]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error( + "Failed to retrieve event with id {}, which was " + "requested to show the replyTo for event {}", + replyTo, + id.toStdString()); + return; + } + emit eventFetched(id, timeline); + }); + } + } + return ids; +} + +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + if ((oldIndex > index || oldIndex == -1) && !pending.contains(currentId) && + ChatPage::instance()->isActiveWindow()) { + readEvent(currentId.toStdString()); + } +} + +void +TimelineModel::readEvent(const std::string &id) +{ + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); +} + +void +TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) +{ + std::vector<QString> ids = internalAddEvents(msgs.chunk); + + if (!ids.empty()) { + beginInsertRows(QModelIndex(), + static_cast<int>(this->eventOrder.size()), + static_cast<int>(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); + } + + prev_batch_token_ = QString::fromStdString(msgs.end); +} + +QString +TimelineModel::displayName(QString id) const +{ + return cache::displayName(room_id_, id).toHtmlEscaped(); +} + +QString +TimelineModel::avatarUrl(QString id) const +{ + return cache::avatarUrl(room_id_, id); +} + +QString +TimelineModel::formatDateSeparator(QDate date) const +{ + auto now = QDateTime::currentDateTime(); + + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } + + return date.toString(fmt); +} + +QString +TimelineModel::escapeEmoji(QString str) const +{ + return utils::replaceEmoji(str); +} + +void +TimelineModel::viewRawMessage(QString id) const +{ + std::string ev = utils::serialize_event(events.value(id)).dump(4); + auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); + Q_UNUSED(dialog); +} + +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + +DecryptionResult +TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = + tr("-- Encrypted Event (No keys found for decryption) --", + "Placeholder, when the message was not decrypted yet or can't be decrypted") + .toStdString(); + + try { + if (!cache::inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return {dummy, false}; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); + dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", + "Placeholder, when the message can't be decrypted, because " + "the DB access failed when trying to lookup the session.") + .toStdString(); + return {dummy, false}; + } + + std::string msg_str; + try { + auto session = cache::getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB access " + "failed.") + .toStdString(); + return {dummy, false}; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the Olm " + "decrytion returned an error, which is passed ad %1") + .arg(e.what()) + .toStdString(); + return {dummy, false}; + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + // relations are unencrypted in content... + if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0) + body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"]; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector<mtx::events::collections::TimelineEvents> temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) + return {temp_events.at(0), true}; + + dummy.content.body = + tr("-- Encrypted Event (Unknown event type) --", + "Placeholder, when the message was decrypted, but we couldn't parse it, because " + "Nheko/mtxclient don't support that event type yet") + .toStdString(); + return {dummy, false}; +} + +void +TimelineModel::replyAction(QString id) +{ + auto event = events.value(id); + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + RelatedInfo related = {}; + related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); + related.related_event = mtx::accessors::event_id(event); + related.type = mtx::accessors::msg_type(event); + related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); + related.quoted_body = utils::getQuoteBody(related); + related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); + related.quoted_formatted_body.remove(QRegularExpression( + "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption)); + nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); + related.room = room_id_; + + ChatPage::instance()->messageReply(related); +} + +void +TimelineModel::readReceiptsAction(QString id) const +{ + MainWindow::instance()->openReadReceiptsDialog(id); +} + +void +TimelineModel::redactEvent(QString id) +{ + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed( + tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); +} + +int +TimelineModel::idToIndex(QString id) const +{ + if (id.isEmpty()) + return -1; + for (int i = 0; i < (int)eventOrder.size(); i++) + if (id == eventOrder[i]) + return i; + return -1; +} + +QString +TimelineModel::indexToId(int index) const +{ + if (index < 0 || index >= (int)eventOrder.size()) + return ""; + return eventOrder[index]; +} + +// Note: this will only be called for our messages +void +TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids) +{ + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + nhlog::ui()->warn("Read index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + } +} + +void +TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json doc = {{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::outboundMegolmSessionExists(room_id)) { + auto data = + olm::encrypt_group_message(room_id, http::client()->device_id(), doc); + + http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + return; + } + + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = + std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc); + + http::client() + ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn( + "[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed( + QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, txn_id, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + emit messageFailed(QString::fromStdString(txn_id)); + return; + } + + for (const auto &user : res.device_keys) { + // Mapping from a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map<std::string, std::string> room_key_msgs; + std::map<std::string, DevicePublicKeys> deviceKeys; + + room_key_msgs.clear(); + deviceKeys.clear(); + + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug( + "ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + try { + if (!mtx::crypto::verify_identity_signature( + json(dev.second), device_id, user_id)) { + nhlog::crypto()->warn( + "failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; + } + } catch (const json::exception &e) { + nhlog::crypto()->warn( + "failed to parse device key json: {}", + e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn( + "failed to verify device key json: {}", + e.what()); + continue; + } + + auto room_key = olm::client() + ->create_room_key_event( + user_id, pks.ed25519, megolm_payload) + .dump(); + + room_key_msgs.emplace(device_id, room_key); + deviceKeys.emplace(device_id, pks); + } + + std::vector<std::string> valid_devices; + valid_devices.reserve(room_key_msgs.size()); + for (auto const &d : room_key_msgs) { + valid_devices.push_back(d.first); + + nhlog::net()->info("{}", d.first); + nhlog::net()->info(" curve25519 {}", + deviceKeys.at(d.first).curve25519); + nhlog::net()->info(" ed25519 {}", + deviceKeys.at(d.first).ed25519); + } + + nhlog::net()->info( + "sending claim request for user {} with {} devices", + user.first, + valid_devices.size()); + + http::client()->claim_keys( + user.first, + valid_devices, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + user.first, + std::placeholders::_1, + std::placeholders::_2)); + + // TODO: Wait before sending the next batch of requests. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } +} + +void +TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::string> &room_keys, + const std::map<std::string, DevicePublicKeys> &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {} {} {}", + err->matrix_error.error, + err->parse_error, + static_cast<int>(err->status_code)); + return; + } + + nhlog::net()->debug("claimed keys for {}", user_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + // Payload with all the to_device message to be sent. + json body; + body["messages"][user_id] = json::object(); + + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + if (pks.find(device_id) == pks.end()) { + nhlog::net()->critical("couldn't find public key for device: {}", + device_id); + continue; + } + + auto id_key = pks.at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); + + if (room_keys.find(device_id) == room_keys.end()) { + nhlog::net()->critical("couldn't find m.room_key for device: {}", + device_id); + continue; + } + + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + + try { + cache::saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + body["messages"][user_id][device_id] = device_msg; + } + + nhlog::net()->info("send_to_device: {}", user_id); + + http::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + (void)keeper; + }); +} + +struct SendMessageVisitor +{ + SendMessageVisitor(const QString &txn_id, TimelineModel *model) + : txn_id_qstr_(txn_id) + , model_(model) + {} + + template<typename T> + void operator()(const mtx::events::Event<T> &) + {} + + template<typename T, + std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> + void operator()(const mtx::events::RoomEvent<T> &msg) + + { + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(txn_id_qstr_.toStdString(), + nlohmann::json(msg.content)); + } else { + QString txn_id_qstr = txn_id_qstr_; + TimelineModel *model = model_; + http::client()->send_room_message<T, mtx::events::EventType::RoomMessage>( + model->room_id_.toStdString(), + txn_id_qstr.toStdString(), + msg.content, + [txn_id_qstr, model](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id_qstr.toStdString(), + err->matrix_error.error, + status_code); + emit model->messageFailed(txn_id_qstr); + } + emit model->messageSent( + txn_id_qstr, QString::fromStdString(res.event_id.to_string())); + }); + } + } + + QString txn_id_qstr_; + TimelineModel *model_; +}; + +void +TimelineModel::processOnePendingMessage() +{ + if (isProcessingPending || pending.isEmpty()) + return; + + isProcessingPending = true; + + QString txn_id_qstr = pending.first(); + + auto event = events.value(txn_id_qstr); + std::visit(SendMessageVisitor{txn_id_qstr, this}, event); +} + +void +TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) +{ + std::visit( + [](auto &msg) { + msg.type = mtx::events::EventType::RoomMessage; + msg.event_id = http::client()->generate_txn_id(); + msg.sender = http::client()->user_id().to_string(); + msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + }, + event); + + internalAddEvents({event}); + + QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event)); + beginInsertRows(QModelIndex(), 0, 0); + pending.push_back(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (!isProcessingPending) + emit nextPendingMessage(); +} + +void +TimelineModel::saveMedia(QString eventId) const +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = QString::fromStdString(mtx::accessors::url(event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event)); + + auto encryptionInfo = mtx::accessors::file(event); + + qml_mtx_events::EventType eventType = toRoomEventType(event); + + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } + + const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + const QString downloadsFolder = + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString openLocation = downloadsFolder + "/" + originalFilename; + + const QString filename = QFileDialog::getSaveFileName( + manager_->getWidget(), dialogTitle, openLocation, filterString); + + if (filename.isEmpty()) + return; + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); +} + +void +TimelineModel::cacheMedia(QString eventId) +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = QString::fromStdString(mtx::accessors::url(event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event)); + + auto encryptionInfo = mtx::accessors::file(event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} + +QString +TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg) +{ + QString temp = + tr("%1 and %2 are typing", + "Multiple users are typing. First argument is a comma separated list of potentially " + "multiple users. Second argument is the last user of that list. (If only one user is " + "typing, %1 is empty. You should still use it in your string though to silence Qt " + "warnings.)", + users.size()); + + if (users.empty()) { + return ""; + } + + QStringList uidWithoutLast; + + auto formatUser = [this, bg](const QString &user_id) -> QString { + auto uncoloredUsername = escapeEmoji(displayName(user_id).toHtmlEscaped()); + QString prefix = + QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name()); + + // color only parts that don't have a font already specified + QString coloredUsername; + int index = 0; + do { + auto startIndex = uncoloredUsername.indexOf("<font", index); + + if (startIndex - index != 0) + coloredUsername += + prefix + + uncoloredUsername.midRef( + index, startIndex > 0 ? startIndex - index : -1) + + "</font>"; + + auto endIndex = uncoloredUsername.indexOf("</font>", startIndex); + if (endIndex > 0) + endIndex += sizeof("</font>") - 1; + + if (endIndex - startIndex != 0) + coloredUsername += + uncoloredUsername.midRef(startIndex, endIndex - startIndex); + + index = endIndex; + } while (index > 0 && index < uncoloredUsername.size()); + + return coloredUsername; + }; + + for (size_t i = 0; i + 1 < users.size(); i++) { + uidWithoutLast.append(formatUser(users[i])); + } + + return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back())); +} + +QString +TimelineModel::formatMemberEvent(QString id) +{ + if (!events.contains(id)) + return ""; + + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(&events[id]); + if (!event) + return ""; + + mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr; + QString prevEventId = QString::fromStdString(event->unsigned_data.replaces_state); + if (!prevEventId.isEmpty()) { + if (!events.contains(prevEventId)) { + http::client()->get_event( + this->room_id_.toStdString(), + event->unsigned_data.replaces_state, + [this, id, prevEventId]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error( + "Failed to retrieve event with id {}, which was " + "requested to show the membership for event {}", + prevEventId.toStdString(), + id.toStdString()); + return; + } + emit eventFetched(id, timeline); + }); + } else { + prevEvent = + std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>( + &events[prevEventId]); + } + } + + QString user = QString::fromStdString(event->state_key); + QString name = escapeEmoji(displayName(user)); + + // see table https://matrix.org/docs/spec/client_server/latest#m-room-member + using namespace mtx::events::state; + switch (event->content.membership) { + case Membership::Invite: + return tr("%1 was invited.").arg(name); + case Membership::Join: + if (prevEvent && prevEvent->content.membership == Membership::Join) { + bool displayNameChanged = + prevEvent->content.display_name != event->content.display_name; + bool avatarChanged = + prevEvent->content.avatar_url != event->content.avatar_url; + + if (displayNameChanged && avatarChanged) + return tr("%1 changed their display name and avatar.").arg(name); + else if (displayNameChanged) + return tr("%1 changed their display name.").arg(name); + else if (avatarChanged) + return tr("%1 changed their avatar.").arg(name); + // the case of nothing changed but join follows join shouldn't happen, so + // just show it as join + } + return tr("%1 joined.").arg(name); + case Membership::Leave: + if (!prevEvent) // Should only ever happen temporarily + return ""; + + if (prevEvent->content.membership == Membership::Invite) { + if (event->state_key == event->sender) + return tr("%1 rejected their invite.").arg(name); + else + return tr("Revoked the invite to %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Join) { + if (event->state_key == event->sender) + return tr("%1 left the room.").arg(name); + else + return tr("Kicked %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Ban) { + return tr("Unbanned %1").arg(name); + } else if (prevEvent->content.membership == Membership::Knock) { + if (event->state_key == event->sender) + return tr("%1 redacted their knock.").arg(name); + else + return tr("Rejected the knock from %1.").arg(name); + } else + return tr("%1 left after having already left!", + "This is a leave event after the user already left and shouln't " + "happen apart from state resets") + .arg(name); + + case Membership::Ban: + return tr("%1 was banned.").arg(name); + case Membership::Knock: + return tr("%1 knocked.").arg(name); + default: + return ""; + } +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h new file mode 100644 index 00000000..f06de5d9 --- /dev/null +++ b/src/timeline/TimelineModel.h @@ -0,0 +1,265 @@ +#pragma once + +#include <QAbstractListModel> +#include <QColor> +#include <QDate> +#include <QHash> +#include <QSet> + +#include <mtxclient/http/errors.hpp> + +#include "CacheCryptoStructs.h" + +namespace mtx::http { +using RequestErr = const std::optional<mtx::http::ClientError> &; +} +namespace mtx::responses { +struct Timeline; +struct Messages; +struct ClaimKeys; +} + +namespace qml_mtx_events { +Q_NAMESPACE + +enum EventType +{ + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + Create, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + GuestAccess, + /// m.room.history_visibility + HistoryVisibility, + /// m.room.join_rules + JoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + UnknownMessage, +}; +Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) +} + +class StateKeeper +{ +public: + StateKeeper(std::function<void()> &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function<void()> fn_; +}; + +struct DecryptionResult +{ + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; +}; + +class TimelineViewManager; + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY + typingUsersChanged) + +public: + explicit TimelineModel(TimelineViewManager *manager, + QString room_id, + QObject *parent = nullptr); + + enum Roles + { + Section, + Type, + TypeString, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + Url, + ThumbnailUrl, + Filename, + Filesize, + MimeType, + Height, + Width, + ProportionalHeight, + Id, + State, + IsEncrypted, + ReplyTo, + RoomName, + RoomTopic, + Dump, + }; + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant data(const QString &id, int role) const; + + bool canFetchMore(const QModelIndex &) const override; + void fetchMore(const QModelIndex &) override; + + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, QColor bg); + Q_INVOKABLE QString formatMemberEvent(QString id); + + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void redactEvent(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; + Q_INVOKABLE void cacheMedia(QString eventId); + Q_INVOKABLE void saveMedia(QString eventId) const; + + void addEvents(const mtx::responses::Timeline &events); + template<class T> + void sendMessage(const T &msg); + +public slots: + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void markEventsAsRead(const std::vector<QString> &event_ids); + QVariantMap getDump(QString eventId) const; + void updateTypingUsers(const std::vector<QString> &users) + { + if (this->typingUsers_ != users) { + this->typingUsers_ = users; + emit typingUsersChanged(typingUsers_); + } + } + std::vector<QString> typingUsers() const { return typingUsers_; } + +private slots: + // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); + void processOnePendingMessage(); + void addPendingMessage(mtx::events::collections::TimelineEvents event); + +signals: + void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); + void currentIndexChanged(int index); + void redactionFailed(QString id); + void eventRedacted(QString id); + void nextPendingMessage(); + void newMessageToSend(mtx::events::collections::TimelineEvents event); + void mediaCached(QString mxcUrl, QString cacheUrl); + void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); + void eventFetched(QString requestingEvent, mtx::events::collections::TimelineEvents event); + void typingUsersChanged(std::vector<QString> users); + +private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const; + std::vector<QString> internalAddEvents( + const std::vector<mtx::events::collections::TimelineEvents> &timeline); + void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); + void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::string> &room_key, + const std::map<std::string, DevicePublicKeys> &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + void updateLastMessage(); + void readEvent(const std::string &id); + + QHash<QString, mtx::events::collections::TimelineEvents> events; + QSet<QString> failed, read; + QList<QString> pending; + std::vector<QString> eventOrder; + + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + bool isProcessingPending = false; + + QString currentId; + std::vector<QString> typingUsers_; + + TimelineViewManager *manager_; + + friend struct SendMessageVisitor; +}; + +template<class T> +void +TimelineModel::sendMessage(const T &msg) +{ + mtx::events::RoomEvent<T> msgCopy = {}; + msgCopy.content = msg; + emit newMessageToSend(msgCopy); +} diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp deleted file mode 100644 index 2b4f979b..00000000 --- a/src/timeline/TimelineView.cpp +++ /dev/null @@ -1,1583 +0,0 @@ -/* - * 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 <boost/variant.hpp> - -#include <QApplication> -#include <QFileInfo> -#include <QTimer> -#include <QtConcurrent> - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Olm.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/FloatingButton.h" -#include "ui/InfoMessage.h" - -#include "timeline/TimelineView.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -//! Maximum number of widgets to keep in the timeline layout. -constexpr int MAX_RETAINED_WIDGETS = 100; -constexpr int MIN_SCROLLBAR_HANDLE = 60; - -//! Retrieve the timestamp of the event represented by the given widget. -QDateTime -getDate(QWidget *widget) -{ - auto item = qobject_cast<TimelineItem *>(widget); - if (item) - return item->descriptionMessage().datetime; - - auto infoMsg = qobject_cast<InfoMessage *>(widget); - if (infoMsg) - return infoMsg->datetime(); - - return QDateTime(); -} - -TimelineView::TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addEvents(timeline); -} - -TimelineView::TimelineView(const QString &room_id, QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - getMessages(); -} - -void -TimelineView::sliderRangeChanged(int min, int max) -{ - Q_UNUSED(min); - - if (!scroll_area_->verticalScrollBar()->isVisible()) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - // If the scrollbar is close to the bottom and a new message - // is added we move the scrollbar. - if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - int currentHeight = scroll_widget_->size().height(); - int diff = currentHeight - oldHeight_; - int newPosition = oldPosition_ + diff; - - // Keep the scroll bar to the bottom if it hasn't been activated yet. - if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible()) - newPosition = max; - - if (lastMessageDirection_ == TimelineDirection::Top) - scroll_area_->verticalScrollBar()->setValue(newPosition); -} - -void -TimelineView::fetchHistory() -{ - if (!isScrollbarActivated() && !isTimelineFinished) { - if (!isVisible()) - return; - - isPaginationInProgress_ = true; - getMessages(); - paginationTimer_->start(2000); - - return; - } - - paginationTimer_->stop(); -} - -void -TimelineView::scrollDown() -{ - int current = scroll_area_->verticalScrollBar()->value(); - int max = scroll_area_->verticalScrollBar()->maximum(); - - // The first time we enter the room move the scroll bar to the bottom. - if (!isInitialized) { - scroll_area_->verticalScrollBar()->setValue(max); - isInitialized = true; - return; - } - - // If the gap is small enough move the scroll bar down. e.g when a new - // message appears. - if (max - current < SCROLL_BAR_GAP) - scroll_area_->verticalScrollBar()->setValue(max); -} - -void -TimelineView::sliderMoved(int position) -{ - if (!scroll_area_->verticalScrollBar()->isVisible()) - return; - - toggleScrollDownButton(); - - // The scrollbar is high enough so we can start retrieving old events. - if (position < SCROLL_BAR_GAP) { - if (isTimelineFinished) - return; - - // Prevent user from moving up when there is pagination in - // progress. - if (isPaginationInProgress_) - return; - - isPaginationInProgress_ = true; - - getMessages(); - } -} - -bool -TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs) -{ - return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start)); -} - -void -TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - // We've reached the start of the timline and there're no more messages. - if (isStartOfTimeline(msgs)) { - nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch", - room_id_.toStdString()); - isTimelineFinished = true; - return; - } - - isTimelineFinished = false; - - // Queue incoming messages to be rendered later. - topMessages_.insert(topMessages_.end(), - std::make_move_iterator(msgs.chunk.begin()), - std::make_move_iterator(msgs.chunk.end())); - - // The RoomList message preview will be updated only if this - // is the first batch of messages received through /messages - // i.e there are no other messages currently present. - if (!topMessages_.empty() && scroll_layout_->count() == 0) - notifyForLastEvent(findFirstViewableEvent(topMessages_)); - - if (isVisible()) { - renderTopEvents(topMessages_); - - // Free up space for new messages. - topMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); - isPaginationInProgress_ = false; -} - -QWidget * -TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction) -{ - using namespace mtx::events; - - using AudioEvent = RoomEvent<msg::Audio>; - using EmoteEvent = RoomEvent<msg::Emote>; - using FileEvent = RoomEvent<msg::File>; - using ImageEvent = RoomEvent<msg::Image>; - using NoticeEvent = RoomEvent<msg::Notice>; - using TextEvent = RoomEvent<msg::Text>; - using VideoEvent = RoomEvent<msg::Video>; - - if (boost::get<RedactionEvent<msg::Redaction>>(&event) != nullptr) { - auto redaction_event = boost::get<RedactionEvent<msg::Redaction>>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); - - QTimer::singleShot(0, this, [event_id, this]() { - if (eventIds_.contains(event_id)) - removeEvent(event_id); - }); - - return nullptr; - } else if (boost::get<StateEvent<state::Encryption>>(&event) != nullptr) { - auto msg = boost::get<StateEvent<state::Encryption>>(event); - auto event_id = QString::fromStdString(msg.event_id); - - if (eventIds_.contains(event_id)) - return nullptr; - - auto item = new InfoMessage(tr("Encryption is enabled"), this); - item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts)); - eventIds_[event_id] = item; - - // Force the next message to have avatar by not providing the current username. - saveMessageInfo("", msg.origin_server_ts, direction); - - return item; - } else if (boost::get<RoomEvent<msg::Audio>>(&event) != nullptr) { - auto audio = boost::get<RoomEvent<msg::Audio>>(event); - return processMessageEvent<AudioEvent, AudioItem>(audio, direction); - } else if (boost::get<RoomEvent<msg::Emote>>(&event) != nullptr) { - auto emote = boost::get<RoomEvent<msg::Emote>>(event); - return processMessageEvent<EmoteEvent>(emote, direction); - } else if (boost::get<RoomEvent<msg::File>>(&event) != nullptr) { - auto file = boost::get<RoomEvent<msg::File>>(event); - return processMessageEvent<FileEvent, FileItem>(file, direction); - } else if (boost::get<RoomEvent<msg::Image>>(&event) != nullptr) { - auto image = boost::get<RoomEvent<msg::Image>>(event); - return processMessageEvent<ImageEvent, ImageItem>(image, direction); - } else if (boost::get<RoomEvent<msg::Notice>>(&event) != nullptr) { - auto notice = boost::get<RoomEvent<msg::Notice>>(event); - return processMessageEvent<NoticeEvent>(notice, direction); - } else if (boost::get<RoomEvent<msg::Text>>(&event) != nullptr) { - auto text = boost::get<RoomEvent<msg::Text>>(event); - return processMessageEvent<TextEvent>(text, direction); - } else if (boost::get<RoomEvent<msg::Video>>(&event) != nullptr) { - auto video = boost::get<RoomEvent<msg::Video>>(event); - return processMessageEvent<VideoEvent, VideoItem>(video, direction); - } else if (boost::get<Sticker>(&event) != nullptr) { - return processMessageEvent<Sticker, StickerItem>(boost::get<Sticker>(event), - direction); - } else if (boost::get<EncryptedEvent<msg::Encrypted>>(&event) != nullptr) { - auto res = parseEncryptedEvent(boost::get<EncryptedEvent<msg::Encrypted>>(event)); - auto widget = parseMessageEvent(res.event, direction); - - if (widget == nullptr) - return nullptr; - - auto item = qobject_cast<TimelineItem *>(widget); - - if (item && res.isDecrypted) - item->markReceived(true); - else if (item && !res.isDecrypted) - item->addKeyRequestAction(); - - return widget; - } - - return nullptr; -} - -DecryptionResult -TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = "-- Encrypted Event (No keys found for decryption) --"; - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = "-- Decryption Error (failed to communicate with DB) --"; - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - "-- Decryption Error (failed to retrieve megolm keys from db) --"; - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = "-- Decryption Error (" + std::string(e.what()) + ") --"; - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - nhlog::crypto()->debug("decrypted event: {}", e.event_id); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector<TimelineEvent> events; - mtx::responses::utils::parse_timeline_events(event_array, events); - - if (events.size() == 1) - return {events.at(0), true}; - - dummy.content.body = "-- Encrypted Event (Unknown event type) --"; - return {dummy, false}; -} - -void -TimelineView::displayReadReceipts(std::vector<TimelineEvent> events) -{ - QtConcurrent::run( - [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() { - std::vector<QString> event_ids; - - for (const auto &e : events) { - if (utils::event_sender(e) == local_user) - event_ids.emplace_back( - QString::fromStdString(utils::event_id(e))); - } - - auto readEvents = - cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString()); - - if (!readEvents.empty()) - emit markReadEvents(readEvents); - }); -} - -void -TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events) -{ - int counter = 0; - - for (const auto &event : events) { - QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom); - - if (item != nullptr) { - addTimelineItem(item, TimelineDirection::Bottom); - counter++; - - // Prevent blocking of the event-loop - // by calling processEvents every 10 items we render. - if (counter % 4 == 0) - QApplication::processEvents(); - } - } - - lastMessageDirection_ = TimelineDirection::Bottom; - - displayReadReceipts(events); - - QApplication::processEvents(); -} - -void -TimelineView::renderTopEvents(const std::vector<TimelineEvent> &events) -{ - std::vector<QWidget *> items; - - // Reset the sender of the first message in the timeline - // cause we're about to insert a new one. - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - - // Parse in reverse order to determine where we should not show sender's name. - for (auto it = events.rbegin(); it != events.rend(); ++it) { - auto item = parseMessageEvent(*it, TimelineDirection::Top); - - if (item != nullptr) - items.push_back(item); - } - - // Reverse again to render them. - std::reverse(items.begin(), items.end()); - - oldPosition_ = scroll_area_->verticalScrollBar()->value(); - oldHeight_ = scroll_widget_->size().height(); - - for (const auto &item : items) - addTimelineItem(item, TimelineDirection::Top); - - lastMessageDirection_ = TimelineDirection::Top; - - QApplication::processEvents(); - - displayReadReceipts(events); - - // If this batch is the first being rendered (i.e the first and the last - // events originate from this batch), set the last sender. - if (lastSender_.isEmpty() && !items.empty()) { - for (const auto &w : items) { - auto timelineItem = qobject_cast<TimelineItem *>(w); - if (timelineItem) { - saveLastMessageInfo(timelineItem->descriptionMessage().userid, - timelineItem->descriptionMessage().datetime); - break; - } - } - } -} - -void -TimelineView::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - bottomMessages_.insert(bottomMessages_.end(), - std::make_move_iterator(timeline.events.begin()), - std::make_move_iterator(timeline.events.end())); - - if (!bottomMessages_.empty()) - notifyForLastEvent(findLastViewableEvent(bottomMessages_)); - - // If the current timeline is open and there are messages to be rendered. - if (isVisible() && !bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - - // Free up space for new messages. - bottomMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } -} - -void -TimelineView::init() -{ - local_user_ = utils::localUser(); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-arrow-down.png"); - scrollDownBtn_ = new FloatingButton(icon, this); - scrollDownBtn_->hide(); - - connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() { - const int max = scroll_area_->verticalScrollBar()->maximum(); - scroll_area_->verticalScrollBar()->setValue(max); - }); - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - scroll_area_ = new QScrollArea(this); - scroll_area_->setWidgetResizable(true); - scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scroll_widget_ = new QWidget(this); - scroll_widget_->setObjectName("scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - scroll_layout_ = new QVBoxLayout(scroll_widget_); - scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - scroll_layout_->setSpacing(0); - scroll_layout_->setObjectName("timelinescrollarea"); - - scroll_area_->setWidget(scroll_widget_); - scroll_area_->setAlignment(Qt::AlignBottom); - - top_layout_->addWidget(scroll_area_); - - setLayout(top_layout_); - - paginationTimer_ = new QTimer(this); - connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - - connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); - - connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); - connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); - - connect( - this, &TimelineView::markReadEvents, this, [this](const std::vector<QString> &event_ids) { - for (const auto &event : event_ids) { - if (eventIds_.contains(event)) { - auto widget = eventIds_[event]; - if (!widget) - return; - - auto item = qobject_cast<TimelineItem *>(widget); - if (!item) - return; - - item->markRead(); - } - } - }); - - connect(scroll_area_->verticalScrollBar(), - SIGNAL(valueChanged(int)), - this, - SLOT(sliderMoved(int))); - connect(scroll_area_->verticalScrollBar(), - SIGNAL(rangeChanged(int, int)), - this, - SLOT(sliderRangeChanged(int, int))); -} - -void -TimelineView::getMessages() -{ - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - emit messagesRetrieved(std::move(res)); - }); -} - -void -TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) - lastSender_ = user_id; - else - firstSender_ = user_id; -} - -bool -TimelineView::isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) { - return (lastSender_ != user_id) || - isDateDifference(lastMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } else { - return (firstSender_ != user_id) || - isDateDifference(firstMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } -} - -void -TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction) -{ - const auto newDate = getDate(item); - - if (direction == TimelineDirection::Bottom) { - QWidget *lastItem = nullptr; - int lastItemPosition = 0; - - if (scroll_layout_->count() > 0) { - lastItemPosition = scroll_layout_->count() - 1; - lastItem = scroll_layout_->itemAt(lastItemPosition)->widget(); - } - - if (lastItem) { - const auto oldDate = getDate(lastItem); - - if (oldDate.daysTo(newDate) != 0) { - auto separator = new DateSeparator(newDate, this); - - if (separator) - pushTimelineItem(separator, direction); - } - } - - pushTimelineItem(item, direction); - } else { - if (scroll_layout_->count() > 0) { - const auto firstItem = scroll_layout_->itemAt(0)->widget(); - - if (firstItem) { - const auto oldDate = getDate(firstItem); - - if (newDate.daysTo(oldDate) != 0) { - auto separator = new DateSeparator(oldDate); - - if (separator) - pushTimelineItem(separator, direction); - } - } - } - - pushTimelineItem(item, direction); - } -} - -void -TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) -{ - nhlog::ui()->debug("[{}] message was received by the server", txn_id); - if (!pending_msgs_.isEmpty() && - pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet - auto msg = pending_msgs_.dequeue(); - msg.event_id = event_id; - - if (msg.widget) { - msg.widget->setEventId(event_id); - eventIds_[event_id] = msg.widget; - - // If the response comes after we have received the event from sync - // we've already marked the widget as received. - if (!msg.widget->isReceived()) { - msg.widget->markReceived(msg.is_encrypted); - cache::client()->addPendingReceipt(room_id_, event_id); - pending_sent_msgs_.append(msg); - } - } else { - nhlog::ui()->warn("[{}] received message response for invalid widget", - txn_id); - } - } - - sendNextPendingMessage(); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - - TimelineItem *view_item = - new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_); - - PendingMessage message; - message.ty = ty; - message.txn_id = http::client()->generate_txn_id(); - message.body = body; - message.widget = view_item; - - try { - message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check encryption status of room {}", e.what()); - view_item->deleteLater(); - - // TODO: Send a notification to the user. - - return; - } - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - handleNewUserMessage(message); -} - -void -TimelineView::handleNewUserMessage(PendingMessage msg) -{ - pending_msgs_.enqueue(msg); - if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); -} - -void -TimelineView::sendNextPendingMessage() -{ - if (pending_msgs_.size() == 0) - return; - - using namespace mtx::events; - - PendingMessage &m = pending_msgs_.head(); - - nhlog::ui()->debug("[{}] sending next queued message", m.txn_id); - - if (m.widget) - m.widget->markSent(); - - if (m.is_encrypted) { - nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id); - prepareEncryptedMessage(std::move(m)); - return; - } - - switch (m.ty) { - case mtx::events::MessageType::Audio: { - http::client()->send_room_message<msg::Audio, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Audio>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Image: { - http::client()->send_room_message<msg::Image, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Image>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Video: { - http::client()->send_room_message<msg::Video, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Video>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::File: { - http::client()->send_room_message<msg::File, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::File>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Text: { - http::client()->send_room_message<msg::Text, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Text>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Emote: { - http::client()->send_room_message<msg::Emote, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Emote>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - break; - } - default: - nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); - break; - } -} - -void -TimelineView::notifyForLastEvent() -{ - if (scroll_layout_->count() == 0) { - nhlog::ui()->error("notifyForLastEvent called with empty timeline"); - return; - } - - auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); - - if (!lastItem) - return; - - auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget()); - - if (lastTimelineItem) - emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); - else - nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString()); -} - -void -TimelineView::notifyForLastEvent(const TimelineEvent &event) -{ - auto descInfo = utils::getMessageDescription(event, local_user_, room_id_); - - if (!descInfo.timestamp.isEmpty()) - emit updateLastTimelineMessage(room_id_, descInfo); -} - -bool -TimelineView::isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &local_userid) -{ - if (sender != local_userid) - return false; - - auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; - - return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || - std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); -} - -void -TimelineView::removePendingMessage(const std::string &txn_id) -{ - if (txn_id.empty()) - return; - - for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - int index = std::distance(pending_sent_msgs_.begin(), it); - pending_sent_msgs_.removeAt(index); - - if (pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); - - nhlog::ui()->debug("[{}] removed message with sync", txn_id); - } - } - for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - if (it->widget) { - it->widget->markReceived(it->is_encrypted); - - // TODO: update when a solution for encrypted messages is available. - if (!it->is_encrypted) - cache::client()->addPendingReceipt(room_id_, it->event_id); - } - - nhlog::ui()->debug("[{}] received sync before message response", txn_id); - return; - } - } -} - -void -TimelineView::handleFailedMessage(const std::string &txn_id) -{ - Q_UNUSED(txn_id); - // Note: We do this even if the message has already been echoed. - QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); -} - -void -TimelineView::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineView::readLastEvent() const -{ - if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled()) - return; - - const auto eventId = getLastEventId(); - - if (!eventId.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - eventId.toStdString(), - [this, eventId](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read event ({}, {})", - room_id_.toStdString(), - eventId.toStdString()); - } - }); -} - -QString -TimelineView::getLastEventId() const -{ - auto index = scroll_layout_->count(); - - // Search backwards for the first event that has a valid event id. - while (index > 0) { - --index; - - auto lastItem = scroll_layout_->itemAt(index); - auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget()); - - if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty()) - return lastTimelineItem->eventId(); - } - - return QString(""); -} - -void -TimelineView::showEvent(QShowEvent *event) -{ - if (!topMessages_.empty()) { - renderTopEvents(topMessages_); - topMessages_.clear(); - } - - if (!bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - bottomMessages_.clear(); - scrollDown(); - } - - toggleScrollDownButton(); - - readLastEvent(); - - QWidget::showEvent(event); -} - -void -TimelineView::hideEvent(QHideEvent *event) -{ - const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height(); - const auto widgetsNum = scroll_layout_->count(); - - // Remove widgets from the timeline to reduce the memory footprint. - if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS) - clearTimeline(); - - QWidget::hideEvent(event); -} - -bool -TimelineView::event(QEvent *event) -{ - if (event->type() == QEvent::WindowActivate) - readLastEvent(); - - return QWidget::event(event); -} - -void -TimelineView::clearTimeline() -{ - // Delete all widgets. - QLayoutItem *item; - while ((item = scroll_layout_->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - - // The next call to /messages will be without a prev token. - prev_batch_token_.clear(); - eventIds_.clear(); - - // Clear queues with pending messages to be rendered. - bottomMessages_.clear(); - topMessages_.clear(); - - firstSender_.clear(); - lastSender_.clear(); -} - -void -TimelineView::toggleScrollDownButton() -{ - const int maxScroll = scroll_area_->verticalScrollBar()->maximum(); - const int currentScroll = scroll_area_->verticalScrollBar()->value(); - - if (maxScroll - currentScroll > SCROLL_BAR_GAP) { - scrollDownBtn_->show(); - scrollDownBtn_->raise(); - } else { - scrollDownBtn_->hide(); - } -} - -void -TimelineView::removeEvent(const QString &event_id) -{ - if (!eventIds_.contains(event_id)) { - nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", - event_id.toStdString()); - return; - } - - auto removedItem = eventIds_[event_id]; - - // Find the next and the previous widgets in the timeline - auto prevWidget = relativeWidget(removedItem, -1); - auto nextWidget = relativeWidget(removedItem, 1); - - // See if they are timeline items - auto prevItem = qobject_cast<TimelineItem *>(prevWidget); - auto nextItem = qobject_cast<TimelineItem *>(nextWidget); - - // ... or a date separator - auto prevLabel = qobject_cast<DateSeparator *>(prevWidget); - - // If it's a TimelineItem add an avatar. - if (prevItem) { - prevItem->addAvatar(); - } - - if (nextItem) { - nextItem->addAvatar(); - } else if (prevLabel) { - // If there's no chat message after this, and we have a label before us, delete the - // label. - prevLabel->deleteLater(); - } - - // If we deleted the last item in the timeline... - if (!nextItem && prevItem) - saveLastMessageInfo(prevItem->descriptionMessage().userid, - prevItem->descriptionMessage().datetime); - - // If we deleted the first item in the timeline... - if (!prevItem && nextItem) - saveFirstMessageInfo(nextItem->descriptionMessage().userid, - nextItem->descriptionMessage().datetime); - - // If we deleted the only item in the timeline... - if (!prevItem && !nextItem) { - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - lastSender_.clear(); - lastMsgTimestamp_ = QDateTime(); - } - - // Finally remove the event. - removedItem->deleteLater(); - eventIds_.remove(event_id); - - // Update the room list with a view of the last message after - // all events have been processed. - QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); }); -} - -QWidget * -TimelineView::relativeWidget(QWidget *item, int dt) const -{ - int pos = scroll_layout_->indexOf(item); - - if (pos == -1) - return nullptr; - - pos = pos + dt; - - bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1); - - return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget(); -} - -TimelineEvent -TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events) -{ - auto it = std::find_if(events.begin(), events.end(), [](const auto &event) { - return mtx::events::EventType::RoomMessage == utils::event_type(event); - }); - - return (it == std::end(events)) ? events.front() : *it; -} - -TimelineEvent -TimelineView::findLastViewableEvent(const std::vector<TimelineEvent> &events) -{ - auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) { - return (mtx::events::EventType::RoomMessage == utils::event_type(event)) || - (mtx::events::EventType::RoomEncrypted == utils::event_type(event)); - }); - - return (it == std::rend(events)) ? events.back() : *it; -} - -void -TimelineView::saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - updateLastSender(sender, direction); - - if (direction == TimelineDirection::Bottom) - lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); - else - firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); -} - -bool -TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const -{ - // Check if the dates are in a different day. - if (std::abs(first.daysTo(second)) != 0) - return true; - - const uint64_t diffInSeconds = std::abs(first.msecsTo(second)) / 1000; - constexpr uint64_t fifteenMins = 15 * 60; - - return diffInSeconds > fifteenMins; -} - -void -TimelineView::sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err) -{ - if (err) { - const int status_code = static_cast<int>(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id); - return; - } - - emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); -} - -template<> -mtx::events::msg::Audio -toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = m.mime.toStdString(); - audio.info.size = m.media_size; - audio.body = m.filename.toStdString(); - audio.url = m.body.toStdString(); - return audio; -} - -template<> -mtx::events::msg::Image -toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m) -{ - mtx::events::msg::Image image; - image.info.mimetype = m.mime.toStdString(); - image.info.size = m.media_size; - image.body = m.filename.toStdString(); - image.url = m.body.toStdString(); - image.info.h = m.dimensions.height(); - image.info.w = m.dimensions.width(); - return image; -} - -template<> -mtx::events::msg::Video -toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m) -{ - mtx::events::msg::Video video; - video.info.mimetype = m.mime.toStdString(); - video.info.size = m.media_size; - video.body = m.filename.toStdString(); - video.url = m.body.toStdString(); - return video; -} - -template<> -mtx::events::msg::Emote -toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Emote emote; - emote.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - return emote; -} - -template<> -mtx::events::msg::File -toRoomMessage<mtx::events::msg::File>(const PendingMessage &m) -{ - mtx::events::msg::File file; - file.info.mimetype = m.mime.toStdString(); - file.info.size = m.media_size; - file.body = m.filename.toStdString(); - file.url = m.body.toStdString(); - return file; -} - -template<> -mtx::events::msg::Text -toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Text text; - text.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - text.formatted_body = html.toStdString(); - - return text; -} - -void -TimelineView::prepareEncryptedMessage(const PendingMessage &msg) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json content; - - // Serialize the message to the plaintext that will be encrypted. - switch (msg.ty) { - case MessageType::Audio: { - content = json(toRoomMessage<msg::Audio>(msg)); - break; - } - case MessageType::Emote: { - content = json(toRoomMessage<msg::Emote>(msg)); - break; - } - case MessageType::File: { - content = json(toRoomMessage<msg::File>(msg)); - break; - } - case MessageType::Image: { - content = json(toRoomMessage<msg::Image>(msg)); - break; - } - case MessageType::Text: { - content = json(toRoomMessage<msg::Text>(msg)); - break; - } - case MessageType::Video: { - content = json(toRoomMessage<msg::Video>(msg)); - break; - } - default: - break; - } - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - msg.txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - msg.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = std::make_shared<StateKeeper>( - [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, this]( - const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast<int>(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map<std::string, std::string> room_key_msgs; - std::map<std::string, DevicePublicKeys> deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector<std::string> valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineView::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_keys, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast<int>(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h deleted file mode 100644 index b0909b44..00000000 --- a/src/timeline/TimelineView.h +++ /dev/null @@ -1,444 +0,0 @@ -/* - * 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 <QApplication> -#include <QLayout> -#include <QList> -#include <QQueue> -#include <QScrollArea> -#include <QScrollBar> -#include <QStyle> -#include <QStyleOption> -#include <QTimer> - -#include <mtx/events.hpp> -#include <mtx/responses/messages.hpp> - -#include "MatrixClient.h" -#include "timeline/TimelineItem.h" - -class StateKeeper -{ -public: - StateKeeper(std::function<void()> &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function<void()> fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - utils::TimelineEvent event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class FloatingButton; -struct DescInfo; - -// Contains info about a message shown in the history view -// but not yet confirmed by the homeserver through sync. -struct PendingMessage -{ - mtx::events::MessageType ty; - std::string txn_id; - QString body; - QString filename; - QString mime; - uint64_t media_size; - QString event_id; - TimelineItem *widget; - QSize dimensions; - bool is_encrypted = false; -}; - -template<class MessageT> -MessageT -toRoomMessage(const PendingMessage &) = delete; - -template<> -mtx::events::msg::Audio -toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m); - -template<> -mtx::events::msg::Emote -toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m); - -template<> -mtx::events::msg::File -toRoomMessage<mtx::events::msg::File>(const PendingMessage &); - -template<> -mtx::events::msg::Image -toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m); - -template<> -mtx::events::msg::Text -toRoomMessage<mtx::events::msg::Text>(const PendingMessage &); - -template<> -mtx::events::msg::Video -toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m); - -// In which place new TimelineItems should be inserted. -enum class TimelineDirection -{ - Top, - Bottom, -}; - -class TimelineView : public QWidget -{ - Q_OBJECT - -public: - TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent = 0); - TimelineView(const QString &room_id, QWidget *parent = 0); - - // Add new events at the end of the timeline. - void addEvents(const mtx::responses::Timeline &timeline); - void addUserMessage(mtx::events::MessageType ty, const QString &msg); - - template<class Widget, mtx::events::MessageType MsgType> - void addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions = QSize()); - void updatePendingMessage(const std::string &txn_id, const QString &event_id); - void scrollDown(); - - //! Remove an item from the timeline with the given Event ID. - void removeEvent(const QString &event_id); - void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; } - -public slots: - void sliderRangeChanged(int min, int max); - void sliderMoved(int position); - void fetchHistory(); - - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - - // Whether or not the initial batch has been loaded. - bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; } - - void handleFailedMessage(const std::string &txn_id); - -private slots: - void sendNextPendingMessage(); - -signals: - void updateLastTimelineMessage(const QString &user, const DescInfo &info); - void messagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string &txn_id); - void messageSent(const std::string &txn_id, const QString &event_id); - void markReadEvents(const std::vector<QString> &event_ids); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - bool event(QEvent *event) override; - -private: - using TimelineEvent = mtx::events::collections::TimelineEvents; - - //! Mark our own widgets as read if they have more than one receipt. - void displayReadReceipts(std::vector<TimelineEvent> events); - //! Determine if the start of the timeline is reached from the response of /messages. - bool isStartOfTimeline(const mtx::responses::Messages &msgs); - - QWidget *relativeWidget(QWidget *item, int dt) const; - - DecryptionResult parseEncryptedEvent( - const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); - - void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_key, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - - //! Callback for all message sending. - void sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err); - void prepareEncryptedMessage(const PendingMessage &msg); - - //! Call the /messages endpoint to fill the timeline. - void getMessages(); - //! HACK: Fixing layout flickering when adding to the bottom - //! of the timeline. - void pushTimelineItem(QWidget *item, TimelineDirection dir) - { - setUpdatesEnabled(false); - item->hide(); - - if (dir == TimelineDirection::Top) - scroll_layout_->insertWidget(0, item); - else - scroll_layout_->addWidget(item); - - QTimer::singleShot(0, this, [item, this]() { - item->show(); - item->adjustSize(); - setUpdatesEnabled(true); - }); - } - - //! Decides whether or not to show or hide the scroll down button. - void toggleScrollDownButton(); - void init(); - void addTimelineItem(QWidget *item, - TimelineDirection direction = TimelineDirection::Bottom); - void updateLastSender(const QString &user_id, TimelineDirection direction); - void notifyForLastEvent(); - void notifyForLastEvent(const TimelineEvent &event); - //! Keep track of the sender and the timestamp of the current message. - void saveLastMessageInfo(const QString &sender, const QDateTime &datetime) - { - lastSender_ = sender; - lastMsgTimestamp_ = datetime; - } - void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime) - { - firstSender_ = sender; - firstMsgTimestamp_ = datetime; - } - //! Keep track of the sender and the timestamp of the current message. - void saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction); - - TimelineEvent findFirstViewableEvent(const std::vector<TimelineEvent> &events); - TimelineEvent findLastViewableEvent(const std::vector<TimelineEvent> &events); - - //! Mark the last event as read. - void readLastEvent() const; - //! Whether or not the scrollbar is visible (non-zero height). - bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } - //! Retrieve the event id of the last item. - QString getLastEventId() const; - - template<class Event, class Widget> - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // TODO: Remove this eventually. - template<class Event> - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // For events with custom display widgets. - template<class Event, class Widget> - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // For events without custom display widgets. - // TODO: All events should have custom widgets. - template<class Event> - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // Used to determine whether or not we should prefix a message with the - // sender's name. - bool isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction); - - bool isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &userid); - void removePendingMessage(const std::string &txn_id); - - bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } - - void handleNewUserMessage(PendingMessage msg); - bool isDateDifference(const QDateTime &first, - const QDateTime &second = QDateTime::currentDateTime()) const; - - // Return nullptr if the event couldn't be parsed. - QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction); - - //! Store the event id associated with the given widget. - void saveEventId(QWidget *widget); - //! Remove all widgets from the timeline layout. - void clearTimeline(); - - QVBoxLayout *top_layout_; - QVBoxLayout *scroll_layout_; - - QScrollArea *scroll_area_; - QWidget *scroll_widget_; - - QString firstSender_; - QDateTime firstMsgTimestamp_; - QString lastSender_; - QDateTime lastMsgTimestamp_; - - QString room_id_; - QString prev_batch_token_; - QString local_user_; - - bool isPaginationInProgress_ = false; - - // Keeps track whether or not the user has visited the view. - bool isInitialized = false; - bool isTimelineFinished = false; - bool isInitialSync = true; - - const int SCROLL_BAR_GAP = 200; - - QTimer *paginationTimer_; - - int scroll_height_ = 0; - int previous_max_height_ = 0; - - int oldPosition_; - int oldHeight_; - - FloatingButton *scrollDownBtn_; - - TimelineDirection lastMessageDirection_; - - //! Messages received by sync not added to the timeline. - std::vector<TimelineEvent> bottomMessages_; - //! Messages received by /messages not added to the timeline. - std::vector<TimelineEvent> topMessages_; - - //! Render the given timeline events to the bottom of the timeline. - void renderBottomEvents(const std::vector<TimelineEvent> &events); - //! Render the given timeline events to the top of the timeline. - void renderTopEvents(const std::vector<TimelineEvent> &events); - - // The events currently rendered. Used for duplicate detection. - QMap<QString, QWidget *> eventIds_; - QQueue<PendingMessage> pending_msgs_; - QList<PendingMessage> pending_sent_msgs_; -}; - -template<class Widget, mtx::events::MessageType MsgType> -void -TimelineView::addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - auto trimmed = QFileInfo{filename}.fileName(); // Trim file path. - - auto widget = new Widget(url, trimmed, size, this); - - TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - // Keep track of the sender and the timestamp of the current message. - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = MsgType; - message.txn_id = http::client()->generate_txn_id(); - message.body = url; - message.filename = trimmed; - message.mime = mime; - message.media_size = size; - message.widget = view_item; - message.dimensions = dimensions; - - handleNewUserMessage(message); -} - -template<class Event> -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); - return item; -} - -template<class Event, class Widget> -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - auto eventWidget = new Widget(event); - auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); - - return item; -} - -template<class Event> -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem<Event>(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} - -template<class Event, class Widget> -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem<Event, Widget>(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index feab46a3..a3827501 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,327 +1,355 @@ -/* - * 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 <random> - -#include <QApplication> -#include <QFileInfo> -#include <QSettings> - -#include "Cache.h" +#include "TimelineViewManager.h" + +#include <QMetaType> +#include <QPalette> +#include <QQmlContext> + +#include "ChatPage.h" +#include "ColorImageProvider.h" +#include "DelegateChooser.h" #include "Logging.h" -#include "timeline/TimelineView.h" -#include "timeline/TimelineViewManager.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "UserSettingsPage.h" +#include "dialogs/ImageOverlay.h" -TimelineViewManager::TimelineViewManager(QWidget *parent) - : QStackedWidget(parent) -{} +Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector<QString> &event_ids) +TimelineViewManager::updateColorPalette() { - if (timelineViewExists(room_id)) { - auto view = views_[room_id]; - if (view) - emit view->markReadEvents(event_ids); + userColors.clear(); + + if (settings->theme() == "light") { + QPalette lightActive(/*windowText*/ QColor("#333"), + /*button*/ QColor("#333"), + /*light*/ QColor(), + /*dark*/ QColor(220, 220, 220), + /*mid*/ QColor(), + /*text*/ QColor("#333"), + /*bright_text*/ QColor(), + /*base*/ QColor(220, 220, 220), + /*window*/ QColor("white")); + lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); + lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); + lightActive.setColor(QPalette::Link, QColor("#0077b5")); + view->rootContext()->setContextProperty("currentActivePalette", lightActive); + view->rootContext()->setContextProperty("currentInactivePalette", lightActive); + } else if (settings->theme() == "dark") { + QPalette darkActive(/*windowText*/ QColor("#caccd1"), + /*button*/ QColor("#caccd1"), + /*light*/ QColor(), + /*dark*/ QColor("#2d3139"), + /*mid*/ QColor(), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor(), + /*base*/ QColor("#2d3139"), + /*window*/ QColor("#202228")); + darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); + darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); + darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); + darkActive.setColor(QPalette::Link, QColor("#38a3d8")); + view->rootContext()->setContextProperty("currentActivePalette", darkActive); + view->rootContext()->setContextProperty("currentInactivePalette", darkActive); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); } } -void -TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) +QColor +TimelineViewManager::userColor(QString id, QColor background) { - auto view = views_[room_id]; - - if (view) - view->removeEvent(event_id); + if (!userColors.contains(id)) + userColors.insert( + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); } -void -TimelineViewManager::queueTextMessage(const QString &msg) +TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent) + : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) + , settings(userSettings) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, msg); + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, + "im.nheko", + 1, + 0, + "MtxEvent", + "Can't instantiate enum!"); + qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser"); + qRegisterMetaType<mtx::events::collections::TimelineEvents>(); + +#ifdef USE_QUICK_VIEW + view = new QQuickView(); + container = QWidget::createWindowContainer(view, parent); +#else + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); +#endif + container->setMinimumSize(200, 200); + view->rootContext()->setContextProperty("timelineManager", this); + updateColorPalette(); + view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); + view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::themeChanged, + this, + &TimelineViewManager::updateColorPalette); } void -TimelineViewManager::queueEmoteMessage(const QString &msg) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; + for (const auto &[room_id, room] : rooms.join) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(room_id)); + const auto &room_model = models.value(QString::fromStdString(room_id)); + room_model->addEvents(room.timeline); + + if (ChatPage::instance()->userSettings()->isTypingNotificationsEnabled()) { + std::vector<QString> typing; + typing.reserve(room.ephemeral.typing.size()); + for (const auto &user : room.ephemeral.typing) { + if (user != http::client()->user_id().to_string()) + typing.push_back(QString::fromStdString(user)); + } + room_model->updateTypingUsers(typing); + } + } - view->addUserMessage(mtx::events::MessageType::Emote, msg); + this->isInitialSync_ = false; + emit initialSyncChanged(false); } void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size, - const QSize &dimensions) +TimelineViewManager::addRoom(const QString &room_id) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); - return; + if (!models.contains(room_id)) { + QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id)); + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + imgProvider, + &MxcImageProvider::addEncryptionInfo); + models.insert(room_id, std::move(newRoom)); } - - auto view = views_[roomid]; - - view->addUserMessage<ImageItem, mtx::events::MessageType::Image>( - url, filename, mime, size, dimensions); } void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::setHistoryView(const QString &room_id) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.file message to a non-managed view"); - return; - } - - auto view = views_[roomid]; + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size); + auto room = models.find(room_id); + if (room != models.end()) { + timeline_ = room.value().data(); + emit activeTimelineChanged(timeline_); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); + } } void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); - return; - } - - auto view = views_[roomid]; + QQuickImageResponse *imgResponse = + imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); + connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->showFullScreen(); + connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() { + timeline_->saveMedia(eventId); + }); + }); } void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::updateReadReceipts(const QString &room_id, + const std::vector<QString> &event_ids) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.video message to a non-managed view"); - return; + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); } - - auto view = views_[roomid]; - - view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size); } void -TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) +TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) { - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - addRoom(it->second, QString::fromStdString(it->first)); - } + for (const auto &e : msgs) { + addRoom(e.first); - sync(rooms); + models.value(e.first)->addEvents(e.second); + } } void -TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) +TimelineViewManager::queueTextMessage(const QString &msg, const std::optional<RelatedInfo> &related) { - for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) { - if (timelineViewExists(it->first)) - return; + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); - // Create a history view with the room events. - TimelineView *view = new TimelineView(it->second, it->first); - views_.emplace(it->first, QSharedPointer<TimelineView>(view)); + if (settings->isMarkdownEnabled()) { + text.formatted_body = utils::markdownToHtml(msg).toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); + // Don't send formatted_body, when we don't need to + if (text.formatted_body.find("<") == std::string::npos) + text.formatted_body = ""; + else + text.format = "org.matrix.custom.html"; } -} -void -TimelineViewManager::initialize(const std::vector<std::string> &rooms) -{ - for (const auto &roomid : rooms) - addRoom(QString::fromStdString(roomid)); -} + if (related) { + QString body; + bool firstLine = true; + for (const auto &line : related->quoted_body.split("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related->quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } -void -TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) -{ - if (timelineViewExists(room_id)) - return; + text.body = QString("%1\n%2").arg(body).arg(msg).toStdString(); - // Create a history view with the room events. - TimelineView *view = new TimelineView(room.timeline, room_id); - views_.emplace(room_id, QSharedPointer<TimelineView>(view)); + // NOTE(Nico): rich replies always need a formatted_body! + text.format = "org.matrix.custom.html"; + if (settings->isMarkdownEnabled()) + text.formatted_body = + utils::getFormattedQuoteBody(*related, utils::markdownToHtml(msg)) + .toStdString(); + else + text.formatted_body = + utils::getFormattedQuoteBody(*related, msg.toHtmlEscaped()).toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + text.relates_to.in_reply_to.event_id = related->related_event; + } - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const QString &room_id) +TimelineViewManager::queueEmoteMessage(const QString &msg) { - if (timelineViewExists(room_id)) - return; + auto html = utils::markdownToHtml(msg); - // Create a history view without any events. - TimelineView *view = new TimelineView(room_id); - views_.emplace(room_id, QSharedPointer<TimelineView>(view)); + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + if (html != msg.trimmed().toHtmlEscaped() && settings->isMarkdownEnabled()) { + emote.formatted_body = html.toStdString(); + emote.format = "org.matrix.custom.html"; + } - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(emote); } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::queueImageMessage(const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions, + const std::optional<RelatedInfo> &related) { - for (const auto &room : rooms.join) { - auto roomid = QString::fromStdString(room.first); - - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("ignoring event from unknown room: {}", - roomid.toStdString()); - continue; - } - - auto view = views_.at(roomid); - - view->addEvents(room.second.timeline); - } + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.body = filename.toStdString(); + image.url = url.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + image.file = file; + + if (related) + image.relates_to.in_reply_to.event_id = related->related_event; + + models.value(roomid)->sendMessage(image); } void -TimelineViewManager::setHistoryView(const QString &room_id) +TimelineViewManager::queueFileMessage( + const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize, + const std::optional<RelatedInfo> &related) { - if (!timelineViewExists(room_id)) { - nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", - room_id.toStdString()); - return; - } + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + file.url = url.toStdString(); + file.file = encryptedFile; - active_room_ = room_id; - auto view = views_.at(room_id); + if (related) + file.relates_to.in_reply_to.event_id = related->related_event; - setCurrentWidget(view.data()); - - view->fetchHistory(); - view->scrollDown(); + models.value(roomid)->sendMessage(file); } -QString -TimelineViewManager::chooseRandomColor() +void +TimelineViewManager::queueAudioMessage(const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const std::optional<RelatedInfo> &related) { - std::random_device random_device; - std::mt19937 engine{random_device()}; - std::uniform_real_distribution<float> dist(0, 1); - - float hue = dist(engine); - float saturation = 0.9; - float value = 0.7; - - int hue_i = hue * 6; - - float f = hue * 6 - hue_i; - - float p = value * (1 - saturation); - float q = value * (1 - f * saturation); - float t = value * (1 - (1 - f) * saturation); - - float r = 0; - float g = 0; - float b = 0; - - if (hue_i == 0) { - r = value; - g = t; - b = p; - } else if (hue_i == 1) { - r = q; - g = value; - b = p; - } else if (hue_i == 2) { - r = p; - g = value; - b = t; - } else if (hue_i == 3) { - r = p; - g = q; - b = value; - } else if (hue_i == 4) { - r = t; - g = p; - b = value; - } else if (hue_i == 5) { - r = value; - g = p; - b = q; - } - - int ri = r * 256; - int gi = g * 256; - int bi = b * 256; + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + audio.file = file; - QColor color(ri, gi, bi); + if (related) + audio.relates_to.in_reply_to.event_id = related->related_event; - return color.name(); + models.value(roomid)->sendMessage(audio); } -bool -TimelineViewManager::hasLoaded() const +void +TimelineViewManager::queueVideoMessage(const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const std::optional<RelatedInfo> &related) { - return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) { - return view.second->hasLoaded(); - }); + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + video.url = url.toStdString(); + video.file = file; + + if (related) + video.relates_to.in_reply_to.event_id = related->related_event; + + models.value(roomid)->sendMessage(video); } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index d23345d3..338101c7 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -1,95 +1,123 @@ -/* - * 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 <QQuickView> +#include <QQuickWidget> #include <QSharedPointer> -#include <QStackedWidget> +#include <QWidget> -#include <mtx.hpp> +#include <mtx/common.hpp> +#include <mtx/responses.hpp> -class QFile; +#include "Cache.h" +#include "Logging.h" +#include "TimelineModel.h" +#include "Utils.h" -class RoomInfoListItem; -class TimelineView; -struct DescInfo; -struct SavedMessages; +class MxcImageProvider; +class ColorImageProvider; +class UserSettings; -class TimelineViewManager : public QStackedWidget +class TimelineViewManager : public QObject { Q_OBJECT -public: - TimelineViewManager(QWidget *parent); - - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector<std::string> &rooms); + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + Q_PROPERTY( + bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) + Q_PROPERTY(QString replyingEvent READ getReplyingEvent WRITE updateReplyingEvent NOTIFY + replyingEventChanged) - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); +public: + TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); + QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } + void addRoom(const QString &room_id); - // Check if all the timelines have been loaded. - bool hasLoaded() const; + void clearAll() { models.clear(); } - static QString chooseRandomColor(); + Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; + Q_INVOKABLE QColor userColor(QString id, QColor background); signals: void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void updateRoomsLastMessage(QString roomid, const DescInfo &info); + void activeTimelineChanged(TimelineModel *timeline); + void initialSyncChanged(bool isInitialSync); + void replyingEventChanged(QString replyingEvent); + void replyClosed(); public slots: + void updateReplyingEvent(const QString &replyingEvent) + { + if (this->replyingEvent_ != replyingEvent) { + this->replyingEvent_ = replyingEvent; + emit replyingEventChanged(replyingEvent_); + } + } + void closeReply() + { + this->updateReplyingEvent(nullptr); + emit replyClosed(); + } + QString getReplyingEvent() const { return replyingEvent_; } void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); - void removeTimelineEvent(const QString &room_id, const QString &event_id); void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs); void setHistoryView(const QString &room_id); - void queueTextMessage(const QString &msg); + void updateColorPalette(); + + void queueTextMessage(const QString &msg, const std::optional<RelatedInfo> &related); void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, uint64_t dsize, - const QSize &dimensions); + const QSize &dimensions, + const std::optional<RelatedInfo> &related); void queueFileMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, - uint64_t dsize); + uint64_t dsize, + const std::optional<RelatedInfo> &related); void queueAudioMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, - uint64_t dsize); + uint64_t dsize, + const std::optional<RelatedInfo> &related); void queueVideoMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, - uint64_t dsize); + uint64_t dsize, + const std::optional<RelatedInfo> &related); private: - //! Check if the given room id is managed by a TimelineView. - bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } - - QString active_room_; - std::map<QString, QSharedPointer<TimelineView>> views_; +#ifdef USE_QUICK_VIEW + QQuickView *view; +#else + QQuickWidget *view; +#endif + QWidget *container; + + MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; + + QHash<QString, QSharedPointer<TimelineModel>> models; + TimelineModel *timeline_ = nullptr; + bool isInitialSync_ = true; + QString replyingEvent_; + + QSharedPointer<UserSettings> settings; + QHash<QString, QColor> userColors; }; diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp deleted file mode 100644 index 72332174..00000000 --- a/src/timeline/widgets/AudioItem.cpp +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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 <QBrush> -#include <QDesktopServices> -#include <QFile> -#include <QFileDialog> -#include <QPainter> -#include <QPixmap> - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/AudioItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int ActionIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -AudioItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - playIcon_.addFile(":/icons/icons/ui/play-sign.png"); - pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png"); - - player_ = new QMediaPlayer; - player_->setMedia(QUrl(url_)); - player_->setVolume(100); - player_->setNotifyInterval(1000); - - connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) { - if (state == QMediaPlayer::StoppedState) { - state_ = AudioState::Play; - player_->setMedia(QUrl(url_)); - update(); - } - }); - - setFixedHeight(Height); -} - -AudioItem::AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, QWidget *parent) - : QWidget(parent) - , url_{QUrl(QString::fromStdString(event.content.url))} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -QSize -AudioItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -AudioItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - if (state_ == AudioState::Play) { - state_ = AudioState::Pause; - player_->play(); - } else { - state_ = AudioState::Play; - player_->pause(); - } - - update(); - } else { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->info("failed to retrieve m.audio content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } -} - -void -AudioItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("error while saving file: {}", e.what()); - } -} - -void -AudioItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); - - resize(computedWidth, Height); - - event->accept(); -} - -void -AudioItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - QIcon icon_; - if (state_ == AudioState::Play) - icon_ = playIcon_; - else - icon_ = pauseIcon_; - - icon_.paint(&painter, - QRect(IconXCenter - ActionIconRadius / 2, - IconYCenter - ActionIconRadius / 2, - ActionIconRadius, - ActionIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h deleted file mode 100644 index c32b7731..00000000 --- a/src/timeline/widgets/AudioItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 <QIcon> -#include <QMediaPlayer> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -class AudioItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - - Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ - durationBackgroundColor) - Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ - durationForegroundColor) - -public: - AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, - QWidget *parent = nullptr); - - AudioItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; } - void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor durationBackgroundColor() const { return durationBgColor_; } - QColor durationForegroundColor() const { return durationFgColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void init(); - - enum class AudioState - { - Play, - Pause, - }; - - AudioState state_ = AudioState::Play; - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent<mtx::events::msg::Audio> event_; - - QMediaPlayer *player_; - - QIcon playIcon_; - QIcon pauseIcon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); - - QColor durationBgColor_ = QColor("black"); - QColor durationFgColor_ = QColor("blue"); -}; diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp deleted file mode 100644 index e97554e2..00000000 --- a/src/timeline/widgets/FileItem.cpp +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 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 <QBrush> -#include <QDesktopServices> -#include <QFile> -#include <QFileDialog> -#include <QPainter> -#include <QPixmap> - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/FileItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int DownloadIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -FileItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); - - setFixedHeight(Height); -} - -FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -void -FileItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString()); -} - -QSize -FileItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -FileItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->warn("failed to retrieve m.file content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } else { - openUrl(); - } -} - -void -FileItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -FileItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); - - resize(computedWidth, Height); - - event->accept(); -} - -void -FileItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - icon_.paint(&painter, - QRect(IconXCenter - DownloadIconRadius / 2, - IconYCenter - DownloadIconRadius / 2, - DownloadIconRadius, - DownloadIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h deleted file mode 100644 index d63cce88..00000000 --- a/src/timeline/widgets/FileItem.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 <QIcon> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -class FileItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, - QWidget *parent = nullptr); - - FileItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void openUrl(); - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent<mtx::events::msg::File> event_; - - QIcon icon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); -}; diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp deleted file mode 100644 index 4ee9e42a..00000000 --- a/src/timeline/widgets/ImageItem.cpp +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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 <QBrush> -#include <QDesktopServices> -#include <QFileDialog> -#include <QFileInfo> -#include <QPainter> -#include <QPixmap> -#include <QUuid> - -#include "Config.h" -#include "ImageItem.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "dialogs/ImageOverlay.h" - -void -ImageItem::downloadMedia(const QUrl &url) -{ - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage); - - http::client()->download(url.toString().toStdString(), - [proxy = std::move(proxy), url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve image {}: {} {}", - url.toString().toStdString(), - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - QPixmap img; - img.loadFromData(QByteArray(data.data(), data.size())); - - emit proxy->imageDownloaded(img); - }); -} - -void -ImageItem::saveImage(const QString &filename, const QByteArray &data) -{ - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -ImageItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - downloadMedia(url_); -} - -ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent) - : QWidget(parent) - , event_{event} -{ - url_ = QString::fromStdString(event.content.url); - text_ = QString::fromStdString(event.content.body); - - init(); -} - -ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - Q_UNUSED(size); - init(); -} - -void -ImageItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString()); -} - -QSize -ImageItem::sizeHint() const -{ - if (image_.isNull()) - return QSize(max_width_, bottom_height_); - - return QSize(width_, height_); -} - -void -ImageItem::setImage(const QPixmap &image) -{ - image_ = image; - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); - update(); -} - -void -ImageItem::mousePressEvent(QMouseEvent *event) -{ - if (!isInteractive_) { - event->accept(); - return; - } - - if (event->button() != Qt::LeftButton) - return; - - if (image_.isNull()) { - openUrl(); - return; - } - - if (textRegion_.contains(event->pos())) { - openUrl(); - } else { - auto imgDialog = new dialogs::ImageOverlay(image_); - imgDialog->show(); - connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs); - } -} - -void -ImageItem::resizeEvent(QResizeEvent *event) -{ - if (!image_) - return QWidget::resizeEvent(event); - - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); -} - -void -ImageItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - - QFontMetrics metrics(font); - const int fontHeight = metrics.height() + metrics.ascent(); - - if (image_.isNull()) { - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); - - setFixedSize(metrics.width(elidedText), fontHeight); - - painter.setFont(font); - painter.setPen(QPen(QColor(66, 133, 244))); - painter.drawText(QPoint(0, fontHeight / 2), elidedText); - - return; - } - - imageRegion_ = QRectF(0, 0, width_, height_); - - QPainterPath path; - path.addRoundedRect(imageRegion_, 5, 5); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, scaled_image_); - painter.drawPath(path); - - // Bottom text section - if (isInteractive_ && underMouse()) { - const int textBoxHeight = fontHeight / 2 + 6; - - textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight); - - QPainterPath textPath; - textPath.addRoundedRect(textRegion_, 0, 0); - - painter.fillPath(textPath, QColor(40, 40, 40, 140)); - - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); - - font.setWeight(QFont::Medium); - painter.setFont(font); - painter.setPen(QPen(QColor(Qt::white))); - - textRegion_.adjust(5, 0, 5, 0); - painter.drawText(textRegion_, Qt::AlignVCenter, elidedText); - } -} - -void -ImageItem::saveAs() -{ - auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_); - - if (filename.isEmpty()) - return; - - const auto url = url_.toString().toStdString(); - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage); - - http::client()->download( - url, - [proxy = std::move(proxy), filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit proxy->imageSaved(filename, QByteArray(data.data(), data.size())); - }); -} diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h deleted file mode 100644 index 65bd962d..00000000 --- a/src/timeline/widgets/ImageItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -namespace dialogs { -class ImageOverlay; -} - -class ImageItem : public QWidget -{ - Q_OBJECT -public: - ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, - QWidget *parent = nullptr); - - ImageItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - -public slots: - //! Show a save as dialog for the image. - void saveAs(); - void setImage(const QPixmap &image); - void saveImage(const QString &filename, const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - //! Whether the user can interact with the displayed image. - bool isInteractive_ = true; - -private: - void init(); - void openUrl(); - void downloadMedia(const QUrl &url); - - int max_width_ = 500; - int max_height_ = 300; - - int width_; - int height_; - - QPixmap scaled_image_; - QPixmap image_; - - QUrl url_; - QString text_; - - int bottom_height_ = 30; - - QRectF textRegion_; - QRectF imageRegion_; - - mtx::events::RoomEvent<mtx::events::msg::Image> event_; -}; - -class StickerItem : public ImageItem -{ - Q_OBJECT - -public: - StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr) - : ImageItem{QString::fromStdString(event.content.url), - QString::fromStdString(event.content.body), - event.content.info.size, - parent} - , event_{event} - { - isInteractive_ = false; - setCursor(Qt::ArrowCursor); - setMouseTracking(false); - setAttribute(Qt::WA_Hover, false); - } - -private: - mtx::events::Sticker event_; -}; diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp deleted file mode 100644 index 4b5dc022..00000000 --- a/src/timeline/widgets/VideoItem.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 <QLabel> -#include <QVBoxLayout> - -#include "Config.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "timeline/widgets/VideoItem.h" - -void -VideoItem::init() -{ - url_ = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); -} - -VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); - - auto layout = new QVBoxLayout(this); - layout->setMargin(0); - layout->setSpacing(0); - - QString link = QString("<a href=%1>%2</a>").arg(url_.toString()).arg(text_); - - label_ = new QLabel(link, this); - label_->setMargin(0); - label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - label_->setOpenExternalLinks(true); - - layout->addWidget(label_); -} - -VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h deleted file mode 100644 index 26fa1c35..00000000 --- a/src/timeline/widgets/VideoItem.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 <QLabel> -#include <QSharedPointer> -#include <QUrl> -#include <QWidget> - -#include <mtx.hpp> - -class VideoItem : public QWidget -{ - Q_OBJECT - -public: - VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, - QWidget *parent = nullptr); - - VideoItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - -private: - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - - QLabel *label_; - - mtx::events::RoomEvent<mtx::events::msg::Video> event_; -}; diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp index 4b4cd272..3589fce5 100644 --- a/src/ui/Avatar.cpp +++ b/src/ui/Avatar.cpp @@ -1,12 +1,14 @@ #include <QPainter> +#include <QSettings> +#include "AvatarProvider.h" #include "Utils.h" #include "ui/Avatar.h" -Avatar::Avatar(QWidget *parent) +Avatar::Avatar(QWidget *parent, int size) : QWidget(parent) + , size_(size) { - size_ = ui::AvatarSize; type_ = ui::AvatarType::Letter; letter_ = "A"; @@ -61,35 +63,41 @@ Avatar::setBackgroundColor(const QColor &color) } void -Avatar::setSize(int size) +Avatar::setLetter(const QString &letter) { - size_ = size; - - if (!image_.isNull()) - pixmap_ = utils::scaleImageToPixmap(image_, size_); - - QFont _font(font()); - _font.setPointSizeF(size_ * (ui::FontSize) / 40); - - setFont(_font); + letter_ = letter; + type_ = ui::AvatarType::Letter; update(); } void -Avatar::setLetter(const QString &letter) +Avatar::setImage(const QString &avatar_url) { - letter_ = letter; - type_ = ui::AvatarType::Letter; - update(); + avatar_url_ = avatar_url; + AvatarProvider::resolve(avatar_url, + static_cast<int>(size_ * pixmap_.devicePixelRatio()), + this, + [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void -Avatar::setImage(const QImage &image) +Avatar::setImage(const QString &room, const QString &user) { - image_ = image; - type_ = ui::AvatarType::Image; - pixmap_ = utils::scaleImageToPixmap(image_, size_); - update(); + room_ = room; + user_ = user; + AvatarProvider::resolve(room, + user, + static_cast<int>(size_ * pixmap_.devicePixelRatio()), + this, + [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void @@ -103,6 +111,8 @@ Avatar::setIcon(const QIcon &icon) void Avatar::paintEvent(QPaintEvent *) { + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); + QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); @@ -116,7 +126,18 @@ Avatar::paintEvent(QPaintEvent *) painter.setPen(Qt::NoPen); painter.setBrush(brush); - painter.drawEllipse(r.center(), hs, hs); + rounded ? painter.drawEllipse(r.center(), hs, hs) + : painter.drawRoundedRect(r, 3, 3); + } else if (painter.isActive() && + abs(pixmap_.devicePixelRatio() - painter.device()->devicePixelRatioF()) > 0.01) { + pixmap_ = + pixmap_.scaled(QSize(size_, size_) * painter.device()->devicePixelRatioF()); + pixmap_.setDevicePixelRatio(painter.device()->devicePixelRatioF()); + + if (!avatar_url_.isEmpty()) + setImage(avatar_url_); + else + setImage(room_, user_); } switch (type_) { @@ -129,7 +150,10 @@ Avatar::paintEvent(QPaintEvent *) } case ui::AvatarType::Image: { QPainterPath ppath; - ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_); + + rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_) + : ppath.addRoundedRect(r, 3, 3); + painter.setClipPath(ppath); painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_), pixmap_); diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h index 41967af5..aea7d3e6 100644 --- a/src/ui/Avatar.h +++ b/src/ui/Avatar.h @@ -15,13 +15,13 @@ class Avatar : public QWidget Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) public: - explicit Avatar(QWidget *parent = 0); + explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize); void setBackgroundColor(const QColor &color); void setIcon(const QIcon &icon); - void setImage(const QImage &image); + void setImage(const QString &avatar_url); + void setImage(const QString &room, const QString &user); void setLetter(const QString &letter); - void setSize(int size); void setTextColor(const QColor &color); QColor backgroundColor() const; @@ -38,10 +38,10 @@ private: ui::AvatarType type_; QString letter_; + QString avatar_url_, room_, user_; QColor background_color_; QColor text_color_; QIcon icon_; - QImage image_; QPixmap pixmap_; int size_; }; diff --git a/src/ui/Badge.h b/src/ui/Badge.h index fd73ad30..748b56fd 100644 --- a/src/ui/Badge.h +++ b/src/ui/Badge.h @@ -16,9 +16,9 @@ class Badge : public OverlayWidget Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition) public: - explicit Badge(QWidget *parent = 0); - explicit Badge(const QIcon &icon, QWidget *parent = 0); - explicit Badge(const QString &text, QWidget *parent = 0); + explicit Badge(QWidget *parent = nullptr); + explicit Badge(const QIcon &icon, QWidget *parent = nullptr); + explicit Badge(const QString &text, QWidget *parent = nullptr); void setBackgroundColor(const QColor &color); void setTextColor(const QColor &color); diff --git a/src/ui/DropShadow.cpp b/src/ui/DropShadow.cpp new file mode 100644 index 00000000..d437975c --- /dev/null +++ b/src/ui/DropShadow.cpp @@ -0,0 +1,108 @@ +#include "DropShadow.h" + +#include <QLinearGradient> +#include <QPainter> + +void +DropShadow::draw(QPainter &painter, + qint16 margin, + qreal radius, + QColor start, + QColor end, + qreal startPosition, + qreal endPosition0, + qreal endPosition1, + qreal width, + qreal height) +{ + painter.setPen(Qt::NoPen); + + QLinearGradient gradient; + gradient.setColorAt(startPosition, start); + gradient.setColorAt(endPosition0, end); + + // Right + QPointF right0(width - margin, height / 2); + QPointF right1(width, height / 2); + gradient.setStart(right0); + gradient.setFinalStop(right1); + painter.setBrush(QBrush(gradient)); + // Deprecated in 5.13: painter.drawRoundRect( + // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - + // margin)), 0.0, 0.0); + painter.drawRoundedRect( + QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), + 0.0, + 0.0); + + // Left + QPointF left0(margin, height / 2); + QPointF left1(0, height / 2); + gradient.setStart(left0); + gradient.setFinalStop(left1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); + + // Top + QPointF top0(width / 2, margin); + QPointF top1(width / 2, 0); + gradient.setStart(top0); + gradient.setFinalStop(top1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); + + // Bottom + QPointF bottom0(width / 2, height - margin); + QPointF bottom1(width / 2, height); + gradient.setStart(bottom0); + gradient.setFinalStop(bottom1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0); + + // BottomRight + QPointF bottomright0(width - margin, height - margin); + QPointF bottomright1(width, height); + gradient.setStart(bottomright0); + gradient.setFinalStop(bottomright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); + + // BottomLeft + QPointF bottomleft0(margin, height - margin); + QPointF bottomleft1(0, height); + gradient.setStart(bottomleft0); + gradient.setFinalStop(bottomleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); + + // TopLeft + QPointF topleft0(margin, margin); + QPointF topleft1(0, 0); + gradient.setStart(topleft0); + gradient.setFinalStop(topleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0); + + // TopRight + QPointF topright0(width - margin, margin); + QPointF topright1(width, 0); + gradient.setStart(topright0); + gradient.setFinalStop(topright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0); + + // Widget + painter.setBrush(QBrush("#FFFFFF")); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawRoundedRect( + QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), + radius, + radius); +} diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h index b7ba1985..6997e1a0 100644 --- a/src/ui/DropShadow.h +++ b/src/ui/DropShadow.h @@ -1,8 +1,8 @@ #pragma once #include <QColor> -#include <QLinearGradient> -#include <QPainter> + +class QPainter; class DropShadow { @@ -16,96 +16,5 @@ public: qreal endPosition0, qreal endPosition1, qreal width, - qreal height) - { - painter.setPen(Qt::NoPen); - - QLinearGradient gradient; - gradient.setColorAt(startPosition, start); - gradient.setColorAt(endPosition0, end); - - // Right - QPointF right0(width - margin, height / 2); - QPointF right1(width, height / 2); - gradient.setStart(right0); - gradient.setFinalStop(right1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), - 0.0, - 0.0); - - // Left - QPointF left0(margin, height / 2); - QPointF left1(0, height / 2); - gradient.setStart(left0); - gradient.setFinalStop(left1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); - - // Top - QPointF top0(width / 2, margin); - QPointF top1(width / 2, 0); - gradient.setStart(top0); - gradient.setFinalStop(top1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); - - // Bottom - QPointF bottom0(width / 2, height - margin); - QPointF bottom1(width / 2, height); - gradient.setStart(bottom0); - gradient.setFinalStop(bottom1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), - 0.0, - 0.0); - - // BottomRight - QPointF bottomright0(width - margin, height - margin); - QPointF bottomright1(width, height); - gradient.setStart(bottomright0); - gradient.setFinalStop(bottomright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); - - // BottomLeft - QPointF bottomleft0(margin, height - margin); - QPointF bottomleft1(0, height); - gradient.setStart(bottomleft0); - gradient.setFinalStop(bottomleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); - - // TopLeft - QPointF topleft0(margin, margin); - QPointF topleft1(0, 0); - gradient.setStart(topleft0); - gradient.setFinalStop(topleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topleft0, topleft1), 0.0, 0.0); - - // TopRight - QPointF topright0(width - margin, margin); - QPointF topright1(width, 0); - gradient.setStart(topright0); - gradient.setFinalStop(topright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topright0, topright1), 0.0, 0.0); - - // Widget - painter.setBrush(QBrush("#FFFFFF")); - painter.setRenderHint(QPainter::Antialiasing); - painter.drawRoundRect( - QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), - radius, - radius); - } + qreal height); }; diff --git a/src/ui/FlatButton.cpp b/src/ui/FlatButton.cpp index a828f582..6660c58d 100644 --- a/src/ui/FlatButton.cpp +++ b/src/ui/FlatButton.cpp @@ -2,6 +2,8 @@ #include <QFontDatabase> #include <QIcon> #include <QMouseEvent> +#include <QPaintEvent> +#include <QPainter> #include <QPainterPath> #include <QResizeEvent> #include <QSignalTransition> diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h index 9c2bf425..3749a0d9 100644 --- a/src/ui/FlatButton.h +++ b/src/ui/FlatButton.h @@ -1,7 +1,5 @@ #pragma once -#include <QPaintEvent> -#include <QPainter> #include <QPushButton> #include <QStateMachine> @@ -20,7 +18,7 @@ class FlatButtonStateMachine : public QStateMachine public: explicit FlatButtonStateMachine(FlatButton *parent); - ~FlatButtonStateMachine(); + ~FlatButtonStateMachine() override; void setOverlayOpacity(qreal opacity); void setCheckedOverlayProgress(qreal opacity); @@ -93,16 +91,16 @@ class FlatButton : public QPushButton Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize) public: - explicit FlatButton(QWidget *parent = 0, + explicit FlatButton(QWidget *parent = nullptr, ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); explicit FlatButton(const QString &text, - QWidget *parent = 0, + QWidget *parent = nullptr, ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); FlatButton(const QString &text, ui::Role role, - QWidget *parent = 0, + QWidget *parent = nullptr, ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - ~FlatButton(); + ~FlatButton() override; void applyPreset(ui::ButtonPreset preset); diff --git a/src/ui/FloatingButton.cpp b/src/ui/FloatingButton.cpp index 74dcd482..f3a09ccd 100644 --- a/src/ui/FloatingButton.cpp +++ b/src/ui/FloatingButton.cpp @@ -1,3 +1,4 @@ +#include <QPainter> #include <QPainterPath> #include "FloatingButton.h" diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp index e9de20cc..27bc0a5f 100644 --- a/src/ui/InfoMessage.cpp +++ b/src/ui/InfoMessage.cpp @@ -2,8 +2,10 @@ #include "Config.h" #include <QDateTime> +#include <QLocale> #include <QPainter> #include <QPen> +#include <QtGlobal> constexpr int VPadding = 6; constexpr int HPadding = 12; @@ -22,7 +24,13 @@ InfoMessage::InfoMessage(QString msg, QWidget *parent) initFont(); QFontMetrics fm{font()}; - width_ = fm.width(msg_) + HPadding * 2; +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // width deprecated in 5.13 + width_ = fm.width(msg_) + HPadding * 2; +#else + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#endif + height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); @@ -54,17 +62,22 @@ DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) { auto now = QDateTime::currentDateTime(); - QString fmt; + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - if (now.date().year() != datetime.date().year()) - fmt = QString("ddd d MMMM yy"); - else - fmt = QString("ddd d MMMM"); + if (now.date().year() == datetime.date().year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } - msg_ = datetime.toString(fmt); + msg_ = datetime.date().toString(fmt); QFontMetrics fm{font()}; - width_ = fm.width(msg_) + HPadding * 2; +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // width deprecated in 5.13 + width_ = fm.width(msg_) + HPadding * 2; +#else + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#endif height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); diff --git a/src/ui/LoadingIndicator.cpp b/src/ui/LoadingIndicator.cpp index c8337089..d2b1240d 100644 --- a/src/ui/LoadingIndicator.cpp +++ b/src/ui/LoadingIndicator.cpp @@ -1,7 +1,8 @@ #include "LoadingIndicator.h" -#include <QPoint> -#include <QtGlobal> +#include <QPaintEvent> +#include <QPainter> +#include <QTimer> LoadingIndicator::LoadingIndicator(QWidget *parent) : QWidget(parent) diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h index e8de0aec..678ef611 100644 --- a/src/ui/LoadingIndicator.h +++ b/src/ui/LoadingIndicator.h @@ -1,20 +1,20 @@ #pragma once #include <QColor> -#include <QPaintEvent> -#include <QPainter> -#include <QTimer> #include <QWidget> +class QPainter; +class QTimer; +class QPaintEvent; class LoadingIndicator : public QWidget { Q_OBJECT Q_PROPERTY(QColor color READ color WRITE setColor) public: - LoadingIndicator(QWidget *parent = 0); + LoadingIndicator(QWidget *parent = nullptr); - void paintEvent(QPaintEvent *e); + void paintEvent(QPaintEvent *e) override; void start(); void stop(); diff --git a/src/ui/OverlayWidget.cpp b/src/ui/OverlayWidget.cpp index ccac0116..a32d86b6 100644 --- a/src/ui/OverlayWidget.cpp +++ b/src/ui/OverlayWidget.cpp @@ -1,5 +1,7 @@ #include "OverlayWidget.h" -#include <QEvent> + +#include <QPainter> +#include <QStyleOption> OverlayWidget::OverlayWidget(QWidget *parent) : QWidget(parent) diff --git a/src/ui/OverlayWidget.h b/src/ui/OverlayWidget.h index 6662479d..ed3ef52d 100644 --- a/src/ui/OverlayWidget.h +++ b/src/ui/OverlayWidget.h @@ -1,10 +1,10 @@ #pragma once #include <QEvent> -#include <QPainter> -#include <QStyleOption> #include <QWidget> +class QPainter; + class OverlayWidget : public QWidget { Q_OBJECT diff --git a/src/ui/Painter.h b/src/ui/Painter.h index 8de39651..4d227a5a 100644 --- a/src/ui/Painter.h +++ b/src/ui/Painter.h @@ -3,6 +3,7 @@ #include <QFontMetrics> #include <QPaintDevice> #include <QPainter> +#include <QtGlobal> class Painter : public QPainter { @@ -20,8 +21,14 @@ public: void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1) { QFontMetrics m(fontMetrics()); - if (textWidth < 0) + if (textWidth < 0) { +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // deprecated in 5.13: textWidth = m.width(text); +#else + textWidth = m.horizontalAdvance(text); +#endif + } drawText((outerw - x - textWidth), y + m.ascent(), text); } @@ -133,8 +140,7 @@ public: { static constexpr QPainter::RenderHint Hints[] = {QPainter::Antialiasing, QPainter::SmoothPixmapTransform, - QPainter::TextAntialiasing, - QPainter::HighQualityAntialiasing}; + QPainter::TextAntialiasing}; auto hints = _painter.renderHints(); for (const auto &hint : Hints) { diff --git a/src/ui/RaisedButton.h b/src/ui/RaisedButton.h index edd5ee4a..47ef1acd 100644 --- a/src/ui/RaisedButton.h +++ b/src/ui/RaisedButton.h @@ -11,9 +11,9 @@ class RaisedButton : public FlatButton Q_OBJECT public: - explicit RaisedButton(QWidget *parent = 0); - explicit RaisedButton(const QString &text, QWidget *parent = 0); - ~RaisedButton(); + explicit RaisedButton(QWidget *parent = nullptr); + explicit RaisedButton(const QString &text, QWidget *parent = nullptr); + ~RaisedButton() override; protected: bool event(QEvent *event) override; diff --git a/src/ui/Ripple.cpp b/src/ui/Ripple.cpp index e22c4a62..ef8a62dd 100644 --- a/src/ui/Ripple.cpp +++ b/src/ui/Ripple.cpp @@ -3,7 +3,7 @@ Ripple::Ripple(const QPoint ¢er, QObject *parent) : QParallelAnimationGroup(parent) - , overlay_(0) + , overlay_(nullptr) , radius_anim_(animate("radius")) , opacity_anim_(animate("opacity")) , radius_(0) diff --git a/src/ui/Ripple.h b/src/ui/Ripple.h index 9184f061..3701fb6c 100644 --- a/src/ui/Ripple.h +++ b/src/ui/Ripple.h @@ -16,8 +16,8 @@ class Ripple : public QParallelAnimationGroup Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) public: - explicit Ripple(const QPoint ¢er, QObject *parent = 0); - Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = 0); + explicit Ripple(const QPoint ¢er, QObject *parent = nullptr); + Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = nullptr); inline void setOverlay(RippleOverlay *overlay); diff --git a/src/ui/RippleOverlay.h b/src/ui/RippleOverlay.h index 9ef91fbf..5d12aff7 100644 --- a/src/ui/RippleOverlay.h +++ b/src/ui/RippleOverlay.h @@ -11,7 +11,7 @@ class RippleOverlay : public OverlayWidget Q_OBJECT public: - explicit RippleOverlay(QWidget *parent = 0); + explicit RippleOverlay(QWidget *parent = nullptr); void addRipple(Ripple *ripple); void addRipple(const QPoint &position, qreal radius = 300); diff --git a/src/ui/SnackBar.cpp b/src/ui/SnackBar.cpp index 8a05d937..5daa697e 100644 --- a/src/ui/SnackBar.cpp +++ b/src/ui/SnackBar.cpp @@ -1,6 +1,6 @@ #include <QPainter> -#include <tweeny/tweeny.h> +#include <tweeny.h> #include "SnackBar.h" diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp index c4582085..4bb7596a 100644 --- a/src/ui/TextField.cpp +++ b/src/ui/TextField.cpp @@ -1,6 +1,6 @@ #include "TextField.h" -#include <QApplication> +#include <QCoreApplication> #include <QEventTransition> #include <QFontDatabase> #include <QPaintEvent> @@ -16,7 +16,7 @@ TextField::TextField(QWidget *parent) QPalette pal; state_machine_ = new TextFieldStateMachine(this); - label_ = 0; + label_ = nullptr; label_font_size_ = 15; show_label_ = false; background_color_ = pal.color(QPalette::Window); @@ -103,23 +103,6 @@ TextField::label() const } void -TextField::setTextColor(const QColor &color) -{ - text_color_ = color; - setStyleSheet(QString("QLineEdit { color: %1; }").arg(color.name())); -} - -QColor -TextField::textColor() const -{ - if (!text_color_.isValid()) { - return QPalette().color(QPalette::Text); - } - - return text_color_; -} - -void TextField::setLabelColor(const QColor &color) { label_color_ = color; @@ -230,9 +213,9 @@ TextFieldStateMachine::TextFieldStateMachine(TextField *parent) normal_state_ = new QState; focused_state_ = new QState; - label_ = 0; - offset_anim_ = 0; - color_anim_ = 0; + label_ = nullptr; + offset_anim_ = nullptr; + color_anim_ = nullptr; progress_ = 0.0; addState(normal_state_); diff --git a/src/ui/TextField.h b/src/ui/TextField.h index 1675a2e0..85d5036d 100644 --- a/src/ui/TextField.h +++ b/src/ui/TextField.h @@ -15,14 +15,13 @@ class TextField : public QLineEdit { Q_OBJECT - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor) Q_PROPERTY(QColor labelColor WRITE setLabelColor READ labelColor) Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor) Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) public: - explicit TextField(QWidget *parent = 0); + explicit TextField(QWidget *parent = nullptr); void setInkColor(const QColor &color); void setBackgroundColor(const QColor &color); @@ -30,12 +29,10 @@ public: void setLabelColor(const QColor &color); void setLabelFontSize(qreal size); void setShowLabel(bool value); - void setTextColor(const QColor &color); void setUnderlineColor(const QColor &color); QColor inkColor() const; QColor labelColor() const; - QColor textColor() const; QColor underlineColor() const; QColor backgroundColor() const; QString label() const; @@ -52,7 +49,6 @@ private: QColor ink_color_; QColor background_color_; QColor label_color_; - QColor text_color_; QColor underline_color_; QString label_text_; TextFieldLabel *label_; diff --git a/src/ui/TextLabel.h b/src/ui/TextLabel.h index 1470d64e..56778dcc 100644 --- a/src/ui/TextLabel.h +++ b/src/ui/TextLabel.h @@ -22,7 +22,7 @@ signals: void contextMenuIsOpening(); protected: - bool eventFilter(QObject *obj, QEvent *event); + bool eventFilter(QObject *obj, QEvent *event) override; }; class TextLabel : public QTextBrowser diff --git a/src/ui/Theme.h b/src/ui/Theme.h index d1d7e2a6..ecff02b5 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -78,7 +78,7 @@ class Theme : public QObject { Q_OBJECT public: - explicit Theme(QObject *parent = 0); + explicit Theme(QObject *parent = nullptr); QColor getColor(const QString &key) const; diff --git a/src/ui/ToggleButton.cpp b/src/ui/ToggleButton.cpp index 755f528f..f9411489 100644 --- a/src/ui/ToggleButton.cpp +++ b/src/ui/ToggleButton.cpp @@ -1,5 +1,5 @@ -#include <QApplication> #include <QColor> +#include <QCoreApplication> #include <QEvent> #include <QPainter> diff --git a/third_party/SingleApplication-3.0.19/.gitignore b/third_party/SingleApplication-3.0.19/.gitignore new file mode 100644 index 00000000..ad390758 --- /dev/null +++ b/third_party/SingleApplication-3.0.19/.gitignore @@ -0,0 +1,9 @@ +/examples/*/*.o +/examples/*/Makefile +/examples/*/moc_*.cpp +/examples/*/moc_predefs.h +/examples/*/*.qmake.stash +/examples/basic/basic +/examples/calculator/calculator +/examples/sending_arguments/sending_arguments +CMakeLists.txt.user diff --git a/third_party/SingleApplication-3.0.19/CHANGELOG.md b/third_party/SingleApplication-3.0.19/CHANGELOG.md new file mode 100644 index 00000000..36f1e261 --- /dev/null +++ b/third_party/SingleApplication-3.0.19/CHANGELOG.md @@ -0,0 +1,237 @@ +Changelog +========= + +__3.0.19__ +---------- + +* Fixed code warning for depricated functions in Qt 5.10 related to `QTime` and `qrand()`. + + _Hennadii Chernyshchyk_ + _Anton Filimonov_ + _Jonas Kvinge_ + +__3.0.18__ +---------- + +* Fallback to standard QApplication class on iOS and Android systems where + the library is not supported. + +* Added Build CI tests to verify the library builds successfully on Linux, Windows and MacOS across multiple Qt versions. + + _Anton Filimonov_ + +__3.0.17__ +---------- + +* Fixed compilation warning/error caused by `geteuid()` on unix based systems. + + _Iakov Kirilenko_ + +* Added CMake support + + _Hennadii Chernyshchyk_ + +__3.0.16__ +---------- + +* Use geteuid and getpwuid to get username on Unix, fallback to environment variable. + + _Jonas Kvinge_ + +__3.0.15__ +---------- + +* Bug Fix: sendMessage() might return false even though data was actually written. + + _Jonas Kvinge_ + +__3.0.14__ +---------- + +* Fixed uninitialised variables in the `SingleApplicationPrivate` constructor. + +__3.0.13a__ +---------- + +* Process socket events asynchronously +* Fix undefined variable error on Windows + + _Francis Giraldeau_ + +__3.0.12a__ +---------- + +* Removed signal handling. + +__3.0.11a__ +---------- + +* Fixed bug where the message sent by the second process was not received + correctly when the message is sent immediately following a connection. + + _Francis Giraldeau_ + +* Refactored code and implemented shared memory block consistency checks + via `qChecksum()` (CRC-16). +* Explicit `qWarning` and `qCritical` when the library is unable to initialise + correctly. + +__3.0.10__ +---------- + +* Removed C style casts and eliminated all clang warnings. Fixed `instanceId` + reading from only one byte in the message deserialization. Cleaned up + serialization code using `QDataStream`. Changed connection type to use + `quint8 enum` rather than `char`. +* Renamed `SingleAppConnectionType` to `ConnectionType`. Added initialization + values to all `ConnectionType` enum cases. + + _Jedidiah Buck McCready_ + +__3.0.9__ +--------- + +* Added SingleApplicationPrivate::primaryPid() as a solution to allow + bringing the primary window of an application to the foreground on + Windows. + + _Eelco van Dam from Peacs BV_ + +__3.0.8__ +--------- + +* Bug fix - changed QApplication::instance() to QCoreApplication::instance() + + _Evgeniy Bazhenov_ + +__3.0.7a__ +---------- + +* Fixed compilation error with Mingw32 in MXE thanks to Vitaly Tonkacheyev. +* Removed QMutex used for thread safe behaviour. The implementation now uses + QCoreApplication::instance() to get an instance to SingleApplication for + memory deallocation. + +__3.0.6a__ +---------- + +* Reverted GetUserName API usage on Windows. Fixed bug with missing library. +* Fixed bug in the Calculator example, preventing it's window to be raised + on Windows. + + Special thanks to Charles Gunawan. + +__3.0.5a__ +---------- + +* Fixed a memory leak in the SingleApplicationPrivate destructor. + + _Sergei Moiseev_ + +__3.0.4a__ +---------- + +* Fixed shadow and uninitialised variable warnings. + + _Paul Walmsley_ + +__3.0.3a__ +---------- + +* Removed Microsoft Windows specific code for getting username due to + multiple problems and compiler differences on Windows platforms. On + Windows the shared memory block in User mode now includes the user's + home path (which contains the user's username). + +* Explicitly getting absolute path of the user's home directory as on Unix + a relative path (`~`) may be returned. + +__3.0.2a__ +---------- + +* Fixed bug on Windows when username containing wide characters causes the + library to crash. + + _Le Liu_ + +__3.0.1a__ +---------- + +* Allows the application path and version to be excluded from the server name + hash. The following flags were added for this purpose: + * `SingleApplication::Mode::ExcludeAppVersion` + * `SingleApplication::Mode::ExcludeAppPath` +* Allow a non elevated process to connect to a local server created by an + elevated process run by the same user on Windows +* Fixes a problem with upper case letters in paths on Windows + + _Le Liu_ + +__v3.0a__ +--------- + +* Deprecated secondary instances count. +* Added a sendMessage() method to send a message to the primary instance. +* Added a receivedMessage() signal, emitted when a message is received from a + secondary instance. +* The SingleApplication constructor's third parameter is now a bool + specifying if the current instance should be allowed to run as a secondary + instance if there is already a primary instance. +* The SingleApplication constructor accept a fourth parameter specifying if + the SingleApplication block should be User-wide or System-wide. +* SingleApplication no longer relies on `applicationName` and + `organizationName` to be set. It instead concatenates all of the following + data and computes a `SHA256` hash which is used as the key of the + `QSharedMemory` block and the `QLocalServer`. Since at least + `applicationFilePath` is always present there is no need to explicitly set + any of the following prior to initialising `SingleApplication`. + * `QCoreApplication::applicationName` + * `QCoreApplication::applicationVersion` + * `QCoreApplication::applicationFilePath` + * `QCoreApplication::organizationName` + * `QCoreApplication::organizationDomain` + * User name or home directory path if in User mode +* The primary instance is no longer notified when a secondary instance had + been started by default. A `Mode` flag for this feature exists. +* Added `instanceNumber()` which represents a unique identifier for each + secondary instance started. When called from the primary instance will + return `0`. + +__v2.4__ +-------- + +* Stability improvements +* Support for secondary instances. +* The library now recovers safely after the primary process has crashed +and the shared memory had not been deleted. + +__v2.3__ +-------- + +* Improved pimpl design and inheritance safety. + + _Vladislav Pyatnichenko_ + +__v2.2__ +-------- + +* The `QAPPLICATION_CLASS` macro can now be defined in the file including the +Single Application header or with a `DEFINES+=` statement in the project file. + +__v2.1__ +-------- + +* A race condition can no longer occur when starting two processes nearly + simultaneously. + + Fix issue [#3](https://github.com/itay-grudev/SingleApplication/issues/3) + +__v2.0__ +-------- + +* SingleApplication is now being passed a reference to `argc` instead of a + copy. + + Fix issue [#1](https://github.com/itay-grudev/SingleApplication/issues/1) + +* Improved documentation. diff --git a/third_party/SingleApplication-3.0.19/CMakeLists.txt b/third_party/SingleApplication-3.0.19/CMakeLists.txt new file mode 100644 index 00000000..076d514d --- /dev/null +++ b/third_party/SingleApplication-3.0.19/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(SingleApplication) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) + +# SingleApplication base class +set(QAPPLICATION_CLASS QCoreApplication CACHE STRING "Inheritance class for SingleApplication") +set_property(CACHE QAPPLICATION_CLASS PROPERTY STRINGS QApplication QGuiApplication QCoreApplication) + +# Libary target +add_library(${PROJECT_NAME} STATIC + singleapplication.cpp + singleapplication_p.cpp + ) + +# Find dependencies +find_package(Qt5Network) +if(QAPPLICATION_CLASS STREQUAL QApplication) + find_package(Qt5 COMPONENTS Widgets REQUIRED) +elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication) + find_package(Qt5 COMPONENTS Gui REQUIRED) +else() + find_package(Qt5 COMPONENTS Core REQUIRED) +endif() +target_compile_definitions(${PROJECT_NAME} PUBLIC QAPPLICATION_CLASS=${QAPPLICATION_CLASS}) + +# Link dependencies +target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Network) +if(QAPPLICATION_CLASS STREQUAL QApplication) + target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Widgets) +elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication) + target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Gui) +else() + target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Core) +endif() + +if(WIN32) + target_link_libraries(${PROJECT_NAME} PRIVATE advapi32) +endif() + +target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) diff --git a/third_party/SingleApplication-3.0.19/LICENSE b/third_party/SingleApplication-3.0.19/LICENSE new file mode 100644 index 00000000..85b2a149 --- /dev/null +++ b/third_party/SingleApplication-3.0.19/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +Copyright (c) Itay Grudev 2015 - 2016 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Note: Some of the examples include code not distributed under the terms of the +MIT License. diff --git a/third_party/SingleApplication-3.0.19/README.md b/third_party/SingleApplication-3.0.19/README.md new file mode 100644 index 00000000..5d609865 --- /dev/null +++ b/third_party/SingleApplication-3.0.19/README.md @@ -0,0 +1,277 @@ +SingleApplication +================= + +This is a replacement of the QtSingleApplication for `Qt5`. + +Keeps the Primary Instance of your Application and kills each subsequent +instances. It can (if enabled) spawn secondary (non-related to the primary) +instances and can send data to the primary instance from secondary instances. + +Usage +----- + +The `SingleApplication` class inherits from whatever `Q[Core|Gui]Application` +class you specify via the `QAPPLICATION_CLASS` macro (`QCoreApplication` is the +default). Further usage is similar to the use of the `Q[Core|Gui]Application` +classes. + +The library sets up a `QLocalServer` and a `QSharedMemory` block. The first +instance of your Application is your Primary Instance. It would check if the +shared memory block exists and if not it will start a `QLocalServer` and listen +for connections. Each subsequent instance of your application would check if the +shared memory block exists and if it does, it will connect to the QLocalServer +to notify the primary instance that a new instance had been started, after which +it would terminate with status code `0`. In the Primary Instance +`SingleApplication` would emit the `instanceStarted()` signal upon detecting +that a new instance had been started. + +The library uses `stdlib` to terminate the program with the `exit()` function. + +You can use the library as if you use any other `QCoreApplication` derived +class: + +```cpp +#include <QApplication> +#include <SingleApplication.h> + +int main( int argc, char* argv[] ) +{ + SingleApplication app( argc, argv ); + + return app.exec(); +} +``` + +To include the library files I would recommend that you add it as a git +submodule to your project and include it's contents with a `.pri` file. Here is +how: + +```bash +git submodule add git@github.com:itay-grudev/SingleApplication.git singleapplication +``` + +**Qmake:** + +Then include the `singleapplication.pri` file in your `.pro` project file. + +```qmake +include(singleapplication/singleapplication.pri) +DEFINES += QAPPLICATION_CLASS=QApplication +``` + +**CMake:** + +Then include the subdirectory in your `CMakeLists.txt` project file. + +```cmake +set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") +add_subdirectory(src/third-party/singleapplication) +``` + +Also don't forget to specify which `QCoreApplication` class your app is using if it +is not `QCoreApplication` as in examples above. + +The `Instance Started` signal +------------------------ + +The SingleApplication class implements a `instanceStarted()` signal. You can +bind to that signal to raise your application's window when a new instance had +been started, for example. + +```cpp +// window is a QWindow instance +QObject::connect( + &app, + &SingleApplication::instanceStarted, + &window, + &QWindow::raise +); +``` + +Using `SingleApplication::instance()` is a neat way to get the +`SingleApplication` instance for binding to it's signals anywhere in your +program. + +__Note:__ On Windows the ability to bring the application windows to the +foreground is restricted. See [Windows specific implementations](Windows.md) +for a workaround and an example implementation. + + +Secondary Instances +------------------- + +If you want to be able to launch additional Secondary Instances (not related to +your Primary Instance) you have to enable that with the third parameter of the +`SingleApplication` constructor. The default is `false` meaning no Secondary +Instances. Here is an example of how you would start a Secondary Instance send +a message with the command line arguments to the primary instance and then shut +down. + +```cpp +int main(int argc, char *argv[]) +{ + SingleApplication app( argc, argv, true ); + + if( app.isSecondary() ) { + app.sendMessage( app.arguments().join(' ')).toUtf8() ); + app.exit( 0 ); + } + + return app.exec(); +} +``` + +*__Note:__ A secondary instance won't cause the emission of the +`instanceStarted()` signal by default. See `SingleApplication::Mode` for more +details.* + +You can check whether your instance is a primary or secondary with the following +methods: + +```cpp +app.isPrimary(); +// or +app.isSecondary(); +``` + +*__Note:__ If your Primary Instance is terminated a newly launched instance +will replace the Primary one even if the Secondary flag has been set.* + +API +--- + +### Members + +```cpp +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 100 ) +``` + +Depending on whether `allowSecondary` is set, this constructor may terminate +your app if there is already a primary instance running. Additional `Options` +can be specified to set whether the SingleApplication block should work +user-wide or system-wide. Additionally the `Mode::SecondaryNotification` may be +used to notify the primary instance whenever a secondary instance had been +started (disabled by default). `timeout` specifies the maximum time in +milliseconds to wait for blocking operations. + +*__Note:__ `argc` and `argv` may be changed as Qt removes arguments that it +recognizes.* + +*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary +and the secondary instance.* + +*__Note:__ Operating system can restrict the shared memory blocks to the same +user, in which case the User/System modes will have no effect and the block will +be user wide.* + +--- + +```cpp +bool SingleApplication::sendMessage( QByteArray message, int timeout = 100 ) +``` + +Sends `message` to the Primary Instance. Uses `timeout` as a the maximum timeout +in milliseconds for blocking functions + +--- + +```cpp +bool SingleApplication::isPrimary() +``` + +Returns if the instance is the primary instance. + +--- + +```cpp +bool SingleApplication::isSecondary() +``` +Returns if the instance is a secondary instance. + +--- + +```cpp +quint32 SingleApplication::instanceId() +``` + +Returns a unique identifier for the current instance. + +--- + +```cpp +qint64 SingleApplication::primaryPid() +``` + +Returns the process ID (PID) of the primary instance. + +### Signals + +```cpp +void SingleApplication::instanceStarted() +``` + +Triggered whenever a new instance had been started, except for secondary +instances if the `Mode::SecondaryNotification` flag is not specified. + +--- + +```cpp +void SingleApplication::receivedMessage( quint32 instanceId, QByteArray message ) +``` + +Triggered whenever there is a message received from a secondary instance. + +--- + +### Flags + +```cpp +enum SingleApplication::Mode +``` + +* `Mode::User` - The SingleApplication block should apply user wide. This adds + user specific data to the key used for the shared memory and server name. + This is the default functionality. +* `Mode::System` – The SingleApplication block applies system-wide. +* `Mode::SecondaryNotification` – Whether to trigger `instanceStarted()` even + whenever secondary instances are started. +* `Mode::ExcludeAppPath` – Excludes the application path from the server name + (and memory block) hash. +* `Mode::ExcludeAppVersion` – Excludes the application version from the server + name (and memory block) hash. + +*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary +and the secondary instance.* + +*__Note:__ Operating system can restrict the shared memory blocks to the same +user, in which case the User/System modes will have no effect and the block will +be user wide.* + +--- + +Versioning +---------- + +Each major version introduces either very significant changes or is not +backwards compatible with the previous version. Minor versions only add +additional features, bug fixes or performance improvements and are backwards +compatible with the previous release. See [`CHANGELOG.md`](CHANGELOG.md) for +more details. + +Implementation +-------------- + +The library is implemented with a QSharedMemory block which is thread safe and +guarantees a race condition will not occur. It also uses a QLocalSocket to +notify the main process that a new instance had been spawned and thus invoke the +`instanceStarted()` signal and for messaging the primary instance. + +Additionally the library can recover from being forcefully killed on *nix +systems and will reset the memory block given that there are no other +instances running. + +License +------- +This library and it's supporting documentation are released under +`The MIT License (MIT)` with the exception of the Qt calculator examples which +is distributed under the BSD license. diff --git a/third_party/SingleApplication-3.0.19/Windows.md b/third_party/SingleApplication-3.0.19/Windows.md new file mode 100644 index 00000000..13c52da0 --- /dev/null +++ b/third_party/SingleApplication-3.0.19/Windows.md @@ -0,0 +1,46 @@ +Windows Specific Implementations +================================ + +Setting the foreground window +----------------------------- + +In the `instanceStarted()` example in the `README` we demonstrated how an +application can bring it's primary instance window whenever a second copy +of the application is started. + +On Windows the ability to bring the application windows to the foreground is +restricted, see [`AllowSetForegroundWindow()`][AllowSetForegroundWindow] for more +details. + +The background process (the primary instance) can bring its windows to the +foreground if it is allowed by the current foreground process (the secondary +instance). To bypass this `SingleApplication` must be initialized with the +`allowSecondary` parameter set to `true` and the `options` parameter must +include `Mode::SecondaryNotification`, See `SingleApplication::Mode` for more +details. + +Here is an example: + +```cpp +if( app.isSecondary() ) { + // This API requires LIBS += User32.lib to be added to the project + AllowSetForegroundWindow( DWORD( app.primaryPid() ) ); +} + +if( app.isPrimary() ) { + QObject::connect( + &app, + &SingleApplication::instanceStarted, + this, + &App::instanceStarted + ); +} +``` + +```cpp +void App::instanceStarted() { + QApplication::setActiveWindow( [window/widget to set to the foreground] ); +} +``` + +[AllowSetForegroundWindow]: https://msdn.microsoft.com/en-us/library/windows/desktop/ms632668.aspx diff --git a/third_party/SingleApplication-3.0.19/singleapplication.cpp b/third_party/SingleApplication-3.0.19/singleapplication.cpp new file mode 100644 index 00000000..8ff8747a --- /dev/null +++ b/third_party/SingleApplication-3.0.19/singleapplication.cpp @@ -0,0 +1,189 @@ +// The MIT License (MIT) +// +// Copyright (c) Itay Grudev 2015 - 2018 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include <QtCore/QElapsedTimer> +#include <QtCore/QThread> +#include <QtCore/QByteArray> +#include <QtCore/QSharedMemory> +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +#include <QtCore/QRandomGenerator> +#else +#include <QtCore/QDateTime> +#endif + +#include "singleapplication.h" +#include "singleapplication_p.h" + +/** + * @brief Constructor. Checks and fires up LocalServer or closes the program + * if another instance already exists + * @param argc + * @param argv + * @param {bool} allowSecondaryInstances + */ +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout ) + : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) +{ + Q_D(SingleApplication); + +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + // On Android and iOS since the library is not supported fallback to + // standard QApplication behaviour by simply returning at this point. + qWarning() << "SingleApplication is not supported on Android and iOS systems."; + return; +#endif + + // Store the current mode of the program + d->options = options; + + // Generating an application ID used for identifying the shared memory + // block and QLocalServer + d->genBlockServerName(); + +#ifdef Q_OS_UNIX + // By explicitly attaching it and then deleting it we make sure that the + // memory is deleted even after the process has crashed on Unix. + d->memory = new QSharedMemory( d->blockServerName ); + d->memory->attach(); + delete d->memory; +#endif + // Guarantee thread safe behaviour with a shared memory block. + d->memory = new QSharedMemory( d->blockServerName ); + + // Create a shared memory block + if( d->memory->create( sizeof( InstancesInfo ) ) ) { + // Initialize the shared memory block + d->memory->lock(); + d->initializeMemoryBlock(); + d->memory->unlock(); + } else { + // Attempt to attach to the memory segment + if( ! d->memory->attach() ) { + qCritical() << "SingleApplication: Unable to attach to shared memory block."; + qCritical() << d->memory->errorString(); + delete d; + ::exit( EXIT_FAILURE ); + } + } + + InstancesInfo* inst = static_cast<InstancesInfo*>( d->memory->data() ); + QElapsedTimer time; + time.start(); + + // Make sure the shared memory block is initialised and in consistent state + while( true ) { + d->memory->lock(); + + if( d->blockChecksum() == inst->checksum ) break; + + if( time.elapsed() > 5000 ) { + qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure."; + d->initializeMemoryBlock(); + } + + d->memory->unlock(); + + // Random sleep here limits the probability of a collision between two racing apps +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + QThread::sleep( QRandomGenerator::global()->bounded( 8u, 18u ) ); +#else + qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max() ); + QThread::sleep( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 ) ); +#endif + } + + if( inst->primary == false) { + d->startPrimary(); + d->memory->unlock(); + return; + } + + // Check if another instance can be started + if( allowSecondary ) { + inst->secondary += 1; + inst->checksum = d->blockChecksum(); + d->instanceNumber = inst->secondary; + d->startSecondary(); + if( d->options & Mode::SecondaryNotification ) { + d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); + } + d->memory->unlock(); + return; + } + + d->memory->unlock(); + + d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); + + delete d; + + ::exit( EXIT_SUCCESS ); +} + +/** + * @brief Destructor + */ +SingleApplication::~SingleApplication() +{ + Q_D(SingleApplication); + delete d; +} + +bool SingleApplication::isPrimary() +{ + Q_D(SingleApplication); + return d->server != nullptr; +} + +bool SingleApplication::isSecondary() +{ + Q_D(SingleApplication); + return d->server == nullptr; +} + +quint32 SingleApplication::instanceId() +{ + Q_D(SingleApplication); + return d->instanceNumber; +} + +qint64 SingleApplication::primaryPid() +{ + Q_D(SingleApplication); + return d->primaryPid(); +} + +bool SingleApplication::sendMessage( QByteArray message, int timeout ) +{ + Q_D(SingleApplication); + + // Nobody to connect to + if( isPrimary() ) return false; + + // Make sure the socket is connected + d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ); + + d->socket->write( message ); + bool dataWritten = d->socket->waitForBytesWritten( timeout ); + d->socket->flush(); + return dataWritten; +} diff --git a/third_party/SingleApplication-3.0.19/singleapplication.h b/third_party/SingleApplication-3.0.19/singleapplication.h new file mode 100644 index 00000000..cb505971 --- /dev/null +++ b/third_party/SingleApplication-3.0.19/singleapplication.h @@ -0,0 +1,135 @@ +// The MIT License (MIT) +// +// Copyright (c) Itay Grudev 2015 - 2018 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#ifndef SINGLE_APPLICATION_H +#define SINGLE_APPLICATION_H + +#include <QtCore/QtGlobal> +#include <QtNetwork/QLocalSocket> + +#ifndef QAPPLICATION_CLASS + #define QAPPLICATION_CLASS QCoreApplication +#endif + +#include QT_STRINGIFY(QAPPLICATION_CLASS) + +class SingleApplicationPrivate; + +/** + * @brief The SingleApplication class handles multiple instances of the same + * Application + * @see QCoreApplication + */ +class SingleApplication : public QAPPLICATION_CLASS +{ + Q_OBJECT + + typedef QAPPLICATION_CLASS app_t; + +public: + /** + * @brief Mode of operation of SingleApplication. + * Whether the block should be user-wide or system-wide and whether the + * primary instance should be notified when a secondary instance had been + * started. + * @note Operating system can restrict the shared memory blocks to the same + * user, in which case the User/System modes will have no effect and the + * block will be user wide. + * @enum + */ + enum Mode { + User = 1 << 0, + System = 1 << 1, + SecondaryNotification = 1 << 2, + ExcludeAppVersion = 1 << 3, + ExcludeAppPath = 1 << 4 + }; + Q_DECLARE_FLAGS(Options, Mode) + + /** + * @brief Intitializes a SingleApplication instance with argc command line + * arguments in argv + * @arg {int &} argc - Number of arguments in argv + * @arg {const char *[]} argv - Supplied command line arguments + * @arg {bool} allowSecondary - Whether to start the instance as secondary + * if there is already a primary instance. + * @arg {Mode} mode - Whether for the SingleApplication block to be applied + * User wide or System wide. + * @arg {int} timeout - Timeout to wait in milliseconds. + * @note argc and argv may be changed as Qt removes arguments that it + * recognizes + * @note Mode::SecondaryNotification only works if set on both the primary + * instance and the secondary instance. + * @note The timeout is just a hint for the maximum time of blocking + * operations. It does not guarantee that the SingleApplication + * initialisation will be completed in given time, though is a good hint. + * Usually 4*timeout would be the worst case (fail) scenario. + * @see See the corresponding QAPPLICATION_CLASS constructor for reference + */ + explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000 ); + ~SingleApplication(); + + /** + * @brief Returns if the instance is the primary instance + * @returns {bool} + */ + bool isPrimary(); + + /** + * @brief Returns if the instance is a secondary instance + * @returns {bool} + */ + bool isSecondary(); + + /** + * @brief Returns a unique identifier for the current instance + * @returns {qint32} + */ + quint32 instanceId(); + + /** + * @brief Returns the process ID (PID) of the primary instance + * @returns {qint64} + */ + qint64 primaryPid(); + + /** + * @brief Sends a message to the primary instance. Returns true on success. + * @param {int} timeout - Timeout for connecting + * @returns {bool} + * @note sendMessage() will return false if invoked from the primary + * instance. + */ + bool sendMessage( QByteArray message, int timeout = 100 ); + +Q_SIGNALS: + void instanceStarted(); + void receivedMessage( quint32 instanceId, QByteArray message ); + +private: + SingleApplicationPrivate *d_ptr; + Q_DECLARE_PRIVATE(SingleApplication) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) + +#endif // SINGLE_APPLICATION_H diff --git a/third_party/SingleApplication-3.0.19/singleapplication_p.cpp b/third_party/SingleApplication-3.0.19/singleapplication_p.cpp new file mode 100644 index 00000000..884fe631 --- /dev/null +++ b/third_party/SingleApplication-3.0.19/singleapplication_p.cpp @@ -0,0 +1,405 @@ +// The MIT License (MIT) +// +// Copyright (c) Itay Grudev 2015 - 2018 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// +// W A R N I N G !!! +// ----------------- +// +// This file is not part of the SingleApplication API. It is used purely as an +// implementation detail. This header file may change from version to +// version without notice, or may even be removed. +// + +#include <cstdlib> +#include <cstddef> + +#include <QtCore/QDir> +#include <QtCore/QByteArray> +#include <QtCore/QDataStream> +#include <QtCore/QCryptographicHash> +#include <QtNetwork/QLocalServer> +#include <QtNetwork/QLocalSocket> + +#include "singleapplication.h" +#include "singleapplication_p.h" + +#ifdef Q_OS_UNIX + #include <unistd.h> + #include <sys/types.h> + #include <pwd.h> +#endif + +#ifdef Q_OS_WIN + #include <windows.h> + #include <lmcons.h> +#endif + +SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) + : q_ptr( q_ptr ) +{ + server = nullptr; + socket = nullptr; + memory = nullptr; + instanceNumber = -1; +} + +SingleApplicationPrivate::~SingleApplicationPrivate() +{ + if( socket != nullptr ) { + socket->close(); + delete socket; + } + + memory->lock(); + InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data()); + if( server != nullptr ) { + server->close(); + delete server; + inst->primary = false; + inst->primaryPid = -1; + inst->checksum = blockChecksum(); + } + memory->unlock(); + + delete memory; +} + +void SingleApplicationPrivate::genBlockServerName() +{ + QCryptographicHash appData( QCryptographicHash::Sha256 ); + appData.addData( "SingleApplication", 17 ); + appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); + + if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ) { + appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); + } + + if( ! (options & SingleApplication::Mode::ExcludeAppPath) ) { +#ifdef Q_OS_WIN + appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); +#else + appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); +#endif + } + + // User level block requires a user specific data in the hash + if( options & SingleApplication::Mode::User ) { +#ifdef Q_OS_WIN + wchar_t username [ UNLEN + 1 ]; + // Specifies size of the buffer on input + DWORD usernameLength = UNLEN + 1; + if( GetUserNameW( username, &usernameLength ) ) { + appData.addData( QString::fromWCharArray(username).toUtf8() ); + } else { + appData.addData( qgetenv("USERNAME") ); + } +#endif +#ifdef Q_OS_UNIX + QByteArray username; + uid_t uid = geteuid(); + struct passwd *pw = getpwuid(uid); + if( pw ) { + username = pw->pw_name; + } + if( username.isEmpty() ) { + username = qgetenv("USER"); + } + appData.addData(username); +#endif + } + + // Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with + // server naming requirements. + blockServerName = appData.result().toBase64().replace("/", "_"); +} + +void SingleApplicationPrivate::initializeMemoryBlock() +{ + InstancesInfo* inst = static_cast<InstancesInfo*>( memory->data() ); + inst->primary = false; + inst->secondary = 0; + inst->primaryPid = -1; + inst->checksum = blockChecksum(); +} + +void SingleApplicationPrivate::startPrimary() +{ + Q_Q(SingleApplication); + + // Successful creation means that no main process exists + // So we start a QLocalServer to listen for connections + QLocalServer::removeServer( blockServerName ); + server = new QLocalServer(); + + // Restrict access to the socket according to the + // SingleApplication::Mode::User flag on User level or no restrictions + if( options & SingleApplication::Mode::User ) { + server->setSocketOptions( QLocalServer::UserAccessOption ); + } else { + server->setSocketOptions( QLocalServer::WorldAccessOption ); + } + + server->listen( blockServerName ); + QObject::connect( + server, + &QLocalServer::newConnection, + this, + &SingleApplicationPrivate::slotConnectionEstablished + ); + + // Reset the number of connections + InstancesInfo* inst = static_cast <InstancesInfo*>( memory->data() ); + + inst->primary = true; + inst->primaryPid = q->applicationPid(); + inst->checksum = blockChecksum(); + + instanceNumber = 0; +} + +void SingleApplicationPrivate::startSecondary() +{ +} + +void SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) +{ + // Connect to the Local Server of the Primary Instance if not already + // connected. + if( socket == nullptr ) { + socket = new QLocalSocket(); + } + + // If already connected - we are done; + if( socket->state() == QLocalSocket::ConnectedState ) + return; + + // If not connect + if( socket->state() == QLocalSocket::UnconnectedState || + socket->state() == QLocalSocket::ClosingState ) { + socket->connectToServer( blockServerName ); + } + + // Wait for being connected + if( socket->state() == QLocalSocket::ConnectingState ) { + socket->waitForConnected( msecs ); + } + + // Initialisation message according to the SingleApplication protocol + if( socket->state() == QLocalSocket::ConnectedState ) { + // Notify the parent that a new instance had been started; + QByteArray initMsg; + QDataStream writeStream(&initMsg, QIODevice::WriteOnly); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + writeStream.setVersion(QDataStream::Qt_5_6); +#endif + + writeStream << blockServerName.toLatin1(); + writeStream << static_cast<quint8>(connectionType); + writeStream << instanceNumber; + quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length())); + writeStream << checksum; + + // The header indicates the message length that follows + QByteArray header; + QDataStream headerStream(&header, QIODevice::WriteOnly); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + headerStream.setVersion(QDataStream::Qt_5_6); +#endif + headerStream << static_cast <quint64>( initMsg.length() ); + + socket->write( header ); + socket->write( initMsg ); + socket->flush(); + socket->waitForBytesWritten( msecs ); + } +} + +quint16 SingleApplicationPrivate::blockChecksum() +{ + return qChecksum( + static_cast <const char *>( memory->data() ), + offsetof( InstancesInfo, checksum ) + ); +} + +qint64 SingleApplicationPrivate::primaryPid() +{ + qint64 pid; + + memory->lock(); + InstancesInfo* inst = static_cast<InstancesInfo*>( memory->data() ); + pid = inst->primaryPid; + memory->unlock(); + + return pid; +} + +/** + * @brief Executed when a connection has been made to the LocalServer + */ +void SingleApplicationPrivate::slotConnectionEstablished() +{ + QLocalSocket *nextConnSocket = server->nextPendingConnection(); + connectionMap.insert(nextConnSocket, ConnectionInfo()); + + QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, + [nextConnSocket, this]() { + auto &info = connectionMap[nextConnSocket]; + Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); + } + ); + + QObject::connect(nextConnSocket, &QLocalSocket::disconnected, + [nextConnSocket, this](){ + connectionMap.remove(nextConnSocket); + nextConnSocket->deleteLater(); + } + ); + + QObject::connect(nextConnSocket, &QLocalSocket::readyRead, + [nextConnSocket, this]() { + auto &info = connectionMap[nextConnSocket]; + switch(info.stage) { + case StageHeader: + readInitMessageHeader(nextConnSocket); + break; + case StageBody: + readInitMessageBody(nextConnSocket); + break; + case StageConnected: + Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId ); + break; + default: + break; + }; + } + ); +} + +void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) +{ + if (!connectionMap.contains( sock )) { + return; + } + + if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ) { + return; + } + + QDataStream headerStream( sock ); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + headerStream.setVersion( QDataStream::Qt_5_6 ); +#endif + + // Read the header to know the message length + quint64 msgLen = 0; + headerStream >> msgLen; + ConnectionInfo &info = connectionMap[sock]; + info.stage = StageBody; + info.msgLen = msgLen; + + if ( sock->bytesAvailable() >= (qint64) msgLen ) { + readInitMessageBody( sock ); + } +} + +void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) +{ + Q_Q(SingleApplication); + + if (!connectionMap.contains( sock )) { + return; + } + + ConnectionInfo &info = connectionMap[sock]; + if( sock->bytesAvailable() < ( qint64 )info.msgLen ) { + return; + } + + // Read the message body + QByteArray msgBytes = sock->read(info.msgLen); + QDataStream readStream(msgBytes); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + readStream.setVersion( QDataStream::Qt_5_6 ); +#endif + + // server name + QByteArray latin1Name; + readStream >> latin1Name; + + // connection type + ConnectionType connectionType = InvalidConnection; + quint8 connTypeVal = InvalidConnection; + readStream >> connTypeVal; + connectionType = static_cast <ConnectionType>( connTypeVal ); + + // instance id + quint32 instanceId = 0; + readStream >> instanceId; + + // checksum + quint16 msgChecksum = 0; + readStream >> msgChecksum; + + const quint16 actualChecksum = qChecksum( msgBytes.constData(), static_cast<quint32>( msgBytes.length() - sizeof( quint16 ) ) ); + + bool isValid = readStream.status() == QDataStream::Ok && + QLatin1String(latin1Name) == blockServerName && + msgChecksum == actualChecksum; + + if( !isValid ) { + sock->close(); + return; + } + + info.instanceId = instanceId; + info.stage = StageConnected; + + if( connectionType == NewInstance || + ( connectionType == SecondaryInstance && + options & SingleApplication::Mode::SecondaryNotification ) ) + { + Q_EMIT q->instanceStarted(); + } + + if (sock->bytesAvailable() > 0) { + Q_EMIT this->slotDataAvailable( sock, instanceId ); + } +} + +void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) +{ + Q_Q(SingleApplication); + Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() ); +} + +void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) +{ + if( closedSocket->bytesAvailable() > 0 ) + Q_EMIT slotDataAvailable( closedSocket, instanceId ); +} diff --git a/third_party/SingleApplication-3.0.19/singleapplication_p.h b/third_party/SingleApplication-3.0.19/singleapplication_p.h new file mode 100644 index 00000000..e2c361fb --- /dev/null +++ b/third_party/SingleApplication-3.0.19/singleapplication_p.h @@ -0,0 +1,99 @@ +// The MIT License (MIT) +// +// Copyright (c) Itay Grudev 2015 - 2016 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// +// W A R N I N G !!! +// ----------------- +// +// This file is not part of the SingleApplication API. It is used purely as an +// implementation detail. This header file may change from version to +// version without notice, or may even be removed. +// + +#ifndef SINGLEAPPLICATION_P_H +#define SINGLEAPPLICATION_P_H + +#include <QtCore/QSharedMemory> +#include <QtNetwork/QLocalServer> +#include <QtNetwork/QLocalSocket> +#include "singleapplication.h" + +struct InstancesInfo { + bool primary; + quint32 secondary; + qint64 primaryPid; + quint16 checksum; +}; + +struct ConnectionInfo { + explicit ConnectionInfo() : + msgLen(0), instanceId(0), stage(0) {} + qint64 msgLen; + quint32 instanceId; + quint8 stage; +}; + +class SingleApplicationPrivate : public QObject { +Q_OBJECT +public: + enum ConnectionType : quint8 { + InvalidConnection = 0, + NewInstance = 1, + SecondaryInstance = 2, + Reconnect = 3 + }; + enum ConnectionStage : quint8 { + StageHeader = 0, + StageBody = 1, + StageConnected = 2, + }; + Q_DECLARE_PUBLIC(SingleApplication) + + SingleApplicationPrivate( SingleApplication *q_ptr ); + ~SingleApplicationPrivate(); + + void genBlockServerName(); + void initializeMemoryBlock(); + void startPrimary(); + void startSecondary(); + void connectToPrimary(int msecs, ConnectionType connectionType ); + quint16 blockChecksum(); + qint64 primaryPid(); + void readInitMessageHeader(QLocalSocket *socket); + void readInitMessageBody(QLocalSocket *socket); + + SingleApplication *q_ptr; + QSharedMemory *memory; + QLocalSocket *socket; + QLocalServer *server; + quint32 instanceNumber; + QString blockServerName; + SingleApplication::Options options; + QMap<QLocalSocket*, ConnectionInfo> connectionMap; + +public Q_SLOTS: + void slotConnectionEstablished(); + void slotDataAvailable( QLocalSocket*, quint32 ); + void slotClientConnectionClosed( QLocalSocket*, quint32 ); +}; + +#endif // SINGLEAPPLICATION_P_H diff --git a/toolchain.cmake b/toolchain.cmake new file mode 100644 index 00000000..85571957 --- /dev/null +++ b/toolchain.cmake @@ -0,0 +1,3 @@ +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) |