Compare commits
No commits in common. "master" and "move-state" have entirely different histories.
master
...
move-state
@ -7,24 +7,11 @@
|
||||
|
||||
set -eu
|
||||
|
||||
FILES=$(find src -type f \( -iname "*.cpp" -o -iname "*.h" \))
|
||||
FILES=$(find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \))
|
||||
|
||||
for f in $FILES
|
||||
do
|
||||
clang-format -i "$f"
|
||||
done;
|
||||
|
||||
QMLFORMAT_PATH=$(command -v qmlformat || true)
|
||||
|
||||
if [ -n "$QMLFORMAT_PATH" ]; then
|
||||
QML_FILES=$(find resources -type f -iname "*.qml")
|
||||
|
||||
for f in $QML_FILES
|
||||
do
|
||||
$QMLFORMAT_PATH -i "$f"
|
||||
done;
|
||||
else
|
||||
echo "qmlformat not found; skipping qml formatting"
|
||||
fi
|
||||
|
||||
git diff --exit-code
|
||||
|
@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Runs the license update
|
||||
# Return codes:
|
||||
# - 1 there are files to be formatted
|
||||
# - 0 everything looks fine
|
||||
|
||||
set -eu
|
||||
|
||||
FILES=$(find src resources/qml -type f \( -iname "*.cpp" -o -iname "*.h" -o -iname "*.qml" \))
|
||||
|
||||
reuse addheader --copyright="Nheko Contributors" --license="GPL-3.0-or-later" $FILES
|
||||
|
||||
git diff --exit-code
|
@ -11,13 +11,13 @@ build-gcc7:
|
||||
variables:
|
||||
CXX: g++-7
|
||||
CC: gcc-7
|
||||
QT_PKG: 512
|
||||
QT_PKG: 510
|
||||
TRAVIS_OS_NAME: linux
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get install -y software-properties-common
|
||||
- add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||
- add-apt-repository ppa:beineri/opt-qt-5.12.9-xenial -y
|
||||
- add-apt-repository ppa:beineri/opt-qt-5.10.1-xenial -y
|
||||
- apt-get update && apt-get -y install --no-install-recommends g++-7 build-essential ninja-build qt${QT_PKG}{base,declarative,tools,multimedia,script,quickcontrols2,svg} liblmdb-dev libgl1-mesa-dev libssl-dev git ccache
|
||||
# need recommended deps for wget
|
||||
- apt-get -y install wget
|
||||
@ -37,7 +37,7 @@ build-gcc7:
|
||||
- cmake -GNinja -H. -Bbuild
|
||||
-DCMAKE_INSTALL_PREFIX=.deps/usr
|
||||
-DHUNTER_ROOT=".hunter"
|
||||
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON -DUSE_BUNDLED_LMDB=OFF
|
||||
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF
|
||||
-DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Release
|
||||
-DCI_BUILD=ON
|
||||
- cmake --build build
|
||||
@ -88,7 +88,7 @@ build-flatpak-amd64:
|
||||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||
tags: [docker]
|
||||
before_script:
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl
|
||||
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
||||
@ -96,10 +96,9 @@ build-flatpak-amd64:
|
||||
- export VERSION=$(git describe)
|
||||
- mkdir -p build-flatpak
|
||||
- cd build-flatpak
|
||||
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.yaml
|
||||
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.json
|
||||
- flatpak build-bundle repo nheko-amd64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
|
||||
after_script:
|
||||
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
|
||||
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-amd64.flatpak
|
||||
cache:
|
||||
key: "$CI_JOB_NAME"
|
||||
@ -116,7 +115,7 @@ build-flatpak-arm64:
|
||||
#image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
|
||||
tags: [docker-arm64]
|
||||
before_script:
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl python3-aiohttp python3-tenacity gir1.2-ostree-1.0
|
||||
- apt-get update && apt-get -y install flatpak-builder git python curl
|
||||
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
- flatpak --noninteractive install --user flathub org.kde.Platform//5.15
|
||||
- flatpak --noninteractive install --user flathub org.kde.Sdk//5.15
|
||||
@ -124,10 +123,9 @@ build-flatpak-arm64:
|
||||
- export VERSION=$(git describe)
|
||||
- mkdir -p build-flatpak
|
||||
- cd build-flatpak
|
||||
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date` for arm64" app ../io.github.NhekoReborn.Nheko.yaml
|
||||
- flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date` for arm64" app ../io.github.NhekoReborn.Nheko.json
|
||||
- flatpak build-bundle repo nheko-arm64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
|
||||
after_script:
|
||||
- (cd ./scripts && ./upload-to-flatpak-repo.sh ../build-flatpak/repo) || true
|
||||
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-arm64.flatpak
|
||||
cache:
|
||||
key: "$CI_JOB_NAME"
|
||||
@ -143,12 +141,9 @@ linting:
|
||||
image: alpine:latest
|
||||
tags: [docker]
|
||||
before_script:
|
||||
- apk update && apk add clang make git python3 py3-pip
|
||||
- export PATH="$PATH:/root/.local/bin"
|
||||
- pip3 install --user reuse
|
||||
- apk update && apk add clang make git
|
||||
script:
|
||||
- make lint
|
||||
- make license
|
||||
|
||||
appimage-amd64:
|
||||
stage: build
|
||||
|
@ -79,7 +79,6 @@ AppDir:
|
||||
- libxv1
|
||||
- libxxf86vm1
|
||||
- libzstd1
|
||||
- qml-module-qt-labs-platform
|
||||
- qml-module-qtgraphicaleffects
|
||||
- qml-module-qtmultimedia
|
||||
- qml-module-qtquick-controls2
|
||||
|
146
CHANGELOG.md
146
CHANGELOG.md
@ -1,151 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.8.2] -- 2021-04-23
|
||||
|
||||
### Highlights
|
||||
|
||||
- Edits
|
||||
- If you made a typo, just press the `Up` key and edit what you wrote.
|
||||
- Messages other users edited will get updated automatically and have a small
|
||||
pen symbol next to them.
|
||||
- Privacy Screen
|
||||
- Blur your messages, when Nheko looses focus, which prevents others from
|
||||
peeking at your messages.
|
||||
- You can configure the timeout of when this happens.
|
||||
- Improved notifications (contributed by lorendb)
|
||||
- No more breakage, because the message included a > on KDE based DEs.
|
||||
- Render html and images where possible in the notification.
|
||||
- Render if a message is a reply or someone sent an emote message more nicely
|
||||
where possible.
|
||||
- Encrypted notifications now show, that the content is encrypted instead of
|
||||
being empty.
|
||||
- Screenshare support in calls on X11 (contributed by trilene)
|
||||
- Share your screen in a call!
|
||||
- Select if your mouse cursor should be shown or not and if your webcam should
|
||||
be included.
|
||||
- SEND MESSAGES AS RAINBOWS! (contributed by LordMZTE)
|
||||
- YES MESSAGES, EMOTES AND NOTICES!
|
||||
|
||||
### Features
|
||||
|
||||
- Set your displayname and avatar from Nheko either globally or per room.
|
||||
(contributed by jedi18)
|
||||
- Show room topic in the room settings.
|
||||
- Double tap a message to reply to it.
|
||||
- Leave a room using `/part` or `/leave`. (contributed by lorendb)
|
||||
- Show mxid when hovering a username or avatar.
|
||||
- Allow opening matrix: uris on Windows.
|
||||
- Disable room pings caused by replies sent via Nheko (unless you are using
|
||||
Element Web/Desktop).
|
||||
|
||||
### Improvements
|
||||
- Userprofile can be closed via the Escape key. No more hotel california!
|
||||
(contributed by lorendb)
|
||||
- Most dialogs are now centered on the Nheko window. (contributed by lorendb)
|
||||
- Update Hungarian translations. (contributed by maxigaz)
|
||||
- Update Estonian translations. (contributed by Priit)
|
||||
- Update Russian translations. (contributed by Alexey Murz and Artem)
|
||||
- Update Swedish translations. (contributed by Emilie)
|
||||
- Update French translations. (contributed by MayeulC, Nicolas Guichard and Carl Schwan)
|
||||
- Allow drag and drop of files on the whole timeline. (contributed by lorendb)
|
||||
- Enable notifications on Haiku. (contributed by kallisti5)
|
||||
- Update scheme handler to the latest matrix: scheme proposal.
|
||||
- Close completers when typing a space after the colon. (contributed by jedi18)
|
||||
- Port room settings to Qml. (contributed by jedi18)
|
||||
- Improved read marker handling. Read marker should now get stuck less often.
|
||||
- Various changes around hover and tap handling in the timeline, which hopefully
|
||||
now works more predicatably.
|
||||
- Buttons in the timeline are now rendered in a box on hover on desktop
|
||||
platforms.
|
||||
- Complete room links in the timeline after typing a # character. (contributed
|
||||
by jedi18)
|
||||
- An improved quick switcher with better rendering and search. (contributed by jedi18)
|
||||
- Some fixes around inline emoji and images.
|
||||
- Jump into new rooms, after you created them. (contrubuted by jedi18)
|
||||
- Improved search in the emoji picker.
|
||||
- Allow disabling certificate checks via the config file.
|
||||
- Use native menus where possible.
|
||||
- Fix video playback on Windows. (contrubuted by jedi18)
|
||||
- Send image messages by pressing Enter. (contributed by salahmak)
|
||||
- Escape closes the upload widget. (contributed by salahmak)
|
||||
- Improve session rotation and sharing in E2EE rooms.
|
||||
|
||||
### Bugfixes
|
||||
- Emojis joined from separate emojis with a 0xfe0f in the middle should now
|
||||
render correctly.
|
||||
- Fix a bug when logging out of a non default profile clearing the wrong
|
||||
profile. (contrubuted by lorendb)
|
||||
- Various fixed around profile handling. (contributed by lorendb)
|
||||
- Focus message input after a reaction. (contributed by jedi18)
|
||||
- Disable native rendering to prevent kerning bugs on non integer scale factors.
|
||||
- Fix duplex call devices not showing up. (contributed by trilene)
|
||||
- Fix a few crashes when leaving a room. (contributed by jedi18)
|
||||
- Fix hidden tags not updating properly. (contributed by jedi18)
|
||||
- Fix some issues with login, when a server had SSO as well as password login
|
||||
enabled (for example matrix.org).
|
||||
- Properly set the dialog flag for dialogs on most platforms. (Wayland does not
|
||||
support that.)
|
||||
- Properly add license to source files.
|
||||
- Fix fingerprint increasing the minimum window size.
|
||||
- Don't send markdown links in the plain text body of events when autocompleting
|
||||
user or room names.
|
||||
- Fix webcam not working in flatpaks.
|
||||
- Fix markdown override in replies.
|
||||
- Fix unsupported events causing errors when saving them. (contributed by
|
||||
anjanik)
|
||||
- Fix exif rotation not being respected anymore in E2EE rooms.
|
||||
- Remove unused qml plugins in the windows package.
|
||||
- Fix broken olm channels automatically when noticed.
|
||||
- Fix pasting not overwriting the selection.
|
||||
- Fix Nheko sometimes overwriting received keys with keys it requested, even if
|
||||
they have a higher minimum index.
|
||||
|
||||
### Packaging changes
|
||||
- Added xcb dependency on X11 based platforms for screensharing (optional)
|
||||
- Bumped lmdbxx version from 0.9.14.0 to 1.0.0, which is a BREAKING change. You
|
||||
can get the new version here: https://github.com/hoytech/lmdbxx/releases
|
||||
(repo changed)
|
||||
- Removed tweeny as a dependency.
|
||||
|
||||
|
||||
## [0.8.1] -- 2021-01-27
|
||||
|
||||
### Features
|
||||
|
||||
- `/plain` and `/md` commands to override the current markdown setting. (contributed by lorendb)
|
||||
- Allow persistent hiding of rooms with a specific tag (or from a community) via a context menu.
|
||||
- Allow open media messages in an external program immediately. (contributed by rnhmjoj)
|
||||
|
||||
### Improvements
|
||||
|
||||
- Use async dbus connection for notifications. (contributed by lorendb)
|
||||
- Update Hungarian translations. (contributed by maxigaz)
|
||||
- Update Finnish translations. (contributed by Priit)
|
||||
- Update Malayalam translations. (contributed by vachan-maker)
|
||||
- Update Dutch translations. (contributed by Glael)
|
||||
- Store splitter size across restarts.
|
||||
- Add a border around the completer. (contributed by lorendb)
|
||||
- Request keys for messages with unknown message indices (once per restart, when they are shown).
|
||||
- Move the database location to XDG_DATA_DIR. (contributed by rnhmjoj)
|
||||
- Reload the timeline after key backup import.
|
||||
- Autoclose completer on `space`, when there are no matches.
|
||||
- Make completer only react, when the mouse cursor is moved.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fix unhandled exception, when a device has no keys.
|
||||
- Fix some cmake warnings regarding GNUInstallDirs.
|
||||
- Fix tags being broken. If you have no tags showing up, you may want to logout and login again.
|
||||
- Fix versionOk being called on the wrong thread. (contributed by Jedi18)
|
||||
- Fix font tags showing up in media message filenames.
|
||||
- Fix user profile in dark themes showing the wrong colors. (contributed by lorendb)
|
||||
- Fix emoji category switching on old Qt versions. (contributed by lorendb)
|
||||
- Fix old messages being replayed after a limited timeline.
|
||||
- Fix empty secrets being returned from the wallet breaking verification.
|
||||
- Make matrix link chat invites create a direct chat.
|
||||
- Fix focus handling on room change or reply button clicks.
|
||||
- Fix username completion deleting the character before it.
|
||||
|
||||
## [0.8.0] -- 2021-01-21
|
||||
|
||||
### Highlights
|
||||
|
@ -33,12 +33,14 @@ option(USE_BUNDLED_CMARK "Use the bundled version of cmark."
|
||||
option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json."
|
||||
${HUNTER_ENABLED})
|
||||
option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL."
|
||||
OFF)
|
||||
${HUNTER_ENABLED})
|
||||
option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${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})
|
||||
option(USE_BUNDLED_QTKEYCHAIN "Use the bundled version of Qt5Keychain."
|
||||
${HUNTER_ENABLED})
|
||||
|
||||
@ -70,16 +72,16 @@ if(${CMAKE_VERSION} VERSION_LESS "3.14.0")
|
||||
endmacro()
|
||||
endif()
|
||||
|
||||
# Include Qt basic functions
|
||||
include(QtCommon)
|
||||
|
||||
project(nheko LANGUAGES CXX C)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
# Include Qt basic functions
|
||||
include(QtCommon)
|
||||
|
||||
set(CPACK_PACKAGE_VERSION_MAJOR "0")
|
||||
set(CPACK_PACKAGE_VERSION_MINOR "8")
|
||||
set(CPACK_PACKAGE_VERSION_PATCH "2")
|
||||
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})
|
||||
@ -127,11 +129,8 @@ endif()
|
||||
if(USE_BUNDLED_LMDB)
|
||||
hunter_add_package(lmdb)
|
||||
find_package(liblmdb CONFIG REQUIRED)
|
||||
|
||||
target_include_directories(liblmdb::lmdb INTERFACE
|
||||
"${HUNTER_INSTALL_PREFIX}/include/lmdb")
|
||||
else()
|
||||
find_package(LMDB REQUIRED)
|
||||
find_package(LMDB)
|
||||
endif()
|
||||
|
||||
#
|
||||
@ -211,7 +210,7 @@ set(SPDLOG_DEBUG_ON false)
|
||||
|
||||
# Windows doesn't handle CMAKE_BUILD_TYPE.
|
||||
if(NOT WIN32)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
if(${CMAKE_BUILD_TYPE} STREQUAL "Debug")
|
||||
set(SPDLOG_DEBUG_ON true)
|
||||
else()
|
||||
set(SPDLOG_DEBUG_ON false)
|
||||
@ -258,6 +257,7 @@ set(SRC_FILES
|
||||
src/dialogs/PreviewUploadOverlay.cpp
|
||||
src/dialogs/ReCaptcha.cpp
|
||||
src/dialogs/ReadReceipts.cpp
|
||||
src/dialogs/RoomSettings.cpp
|
||||
|
||||
# Emoji
|
||||
src/emoji/EmojiModel.cpp
|
||||
@ -271,7 +271,6 @@ set(SRC_FILES
|
||||
src/timeline/TimelineViewManager.cpp
|
||||
src/timeline/TimelineModel.cpp
|
||||
src/timeline/DelegateChooser.cpp
|
||||
src/timeline/Permissions.cpp
|
||||
|
||||
# UI components
|
||||
src/ui/Avatar.cpp
|
||||
@ -282,7 +281,6 @@ set(SRC_FILES
|
||||
src/ui/InfoMessage.cpp
|
||||
src/ui/Label.cpp
|
||||
src/ui/LoadingIndicator.cpp
|
||||
src/ui/NhekoCursorShape.cpp
|
||||
src/ui/NhekoDropArea.cpp
|
||||
src/ui/OverlayModal.cpp
|
||||
src/ui/OverlayWidget.cpp
|
||||
@ -296,18 +294,12 @@ set(SRC_FILES
|
||||
src/ui/ThemeManager.cpp
|
||||
src/ui/ToggleButton.cpp
|
||||
src/ui/UserProfile.cpp
|
||||
src/ui/RoomSettings.cpp
|
||||
|
||||
# Generic notification stuff
|
||||
src/notifications/Manager.cpp
|
||||
|
||||
src/AvatarProvider.cpp
|
||||
src/BlurhashProvider.cpp
|
||||
src/Cache.cpp
|
||||
src/CallDevices.cpp
|
||||
src/CallManager.cpp
|
||||
src/ChatPage.cpp
|
||||
src/Clipboard.cpp
|
||||
src/ColorImageProvider.cpp
|
||||
src/CommunitiesList.cpp
|
||||
src/CommunitiesListItem.cpp
|
||||
@ -321,6 +313,7 @@ set(SRC_FILES
|
||||
src/MatrixClient.cpp
|
||||
src/MxcImageProvider.cpp
|
||||
src/Olm.cpp
|
||||
src/QuickSwitcher.cpp
|
||||
src/RegisterPage.cpp
|
||||
src/RoomInfoListItem.cpp
|
||||
src/RoomList.cpp
|
||||
@ -331,7 +324,6 @@ set(SRC_FILES
|
||||
src/UserInfoWidget.cpp
|
||||
src/UserSettingsPage.cpp
|
||||
src/UsersModel.cpp
|
||||
src/RoomsModel.cpp
|
||||
src/Utils.cpp
|
||||
src/WebRTCSession.cpp
|
||||
src/WelcomePage.cpp
|
||||
@ -363,13 +355,13 @@ if(USE_BUNDLED_MTXCLIENT)
|
||||
FetchContent_Declare(
|
||||
MatrixClient
|
||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
||||
GIT_TAG 5d2f055ea9403770039ddf66b1900f890cd5cde7
|
||||
GIT_TAG v0.4.0
|
||||
)
|
||||
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
|
||||
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
|
||||
FetchContent_MakeAvailable(MatrixClient)
|
||||
else()
|
||||
find_package(MatrixClient 0.5.1 REQUIRED)
|
||||
find_package(MatrixClient 0.4.0 REQUIRED)
|
||||
endif()
|
||||
if(USE_BUNDLED_OLM)
|
||||
include(FetchContent)
|
||||
@ -381,7 +373,7 @@ if(USE_BUNDLED_OLM)
|
||||
set(OLM_TESTS OFF CACHE INTERNAL "")
|
||||
FetchContent_MakeAvailable(Olm)
|
||||
else()
|
||||
find_package(Olm 3 REQUIRED)
|
||||
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/"
|
||||
@ -422,18 +414,8 @@ set_package_properties(nlohmann_json PROPERTIES
|
||||
)
|
||||
|
||||
if(USE_BUNDLED_LMDBXX)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
lmdbxx
|
||||
URL "https://raw.githubusercontent.com/hoytech/lmdbxx/1.0.0/lmdb++.h"
|
||||
DOWNLOAD_NO_EXTRACT TRUE
|
||||
)
|
||||
if(NOT lmdbxx_POPULATED)
|
||||
FetchContent_Populate(lmdbxx)
|
||||
endif()
|
||||
add_library(lmdbxx INTERFACE)
|
||||
target_include_directories(lmdbxx INTERFACE ${lmdbxx_SOURCE_DIR})
|
||||
add_library(lmdbxx::lmdbxx ALIAS lmdbxx)
|
||||
hunter_add_package(lmdbxx)
|
||||
find_package(lmdbxx CONFIG REQUIRED)
|
||||
else()
|
||||
if(NOT LMDBXX_INCLUDE_DIR)
|
||||
find_path(LMDBXX_INCLUDE_DIR
|
||||
@ -449,18 +431,24 @@ else()
|
||||
add_library(lmdbxx::lmdbxx ALIAS lmdbxx)
|
||||
endif()
|
||||
|
||||
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()
|
||||
|
||||
include(FindPkgConfig)
|
||||
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18)
|
||||
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.16 gstreamer-webrtc-1.0>=1.16)
|
||||
if (TARGET PkgConfig::GSTREAMER)
|
||||
add_feature_info(voip ON "GStreamer found. Call support is enabled automatically.")
|
||||
pkg_check_modules(XCB IMPORTED_TARGET xcb xcb-ewmh)
|
||||
if (TARGET PkgConfig::XCB)
|
||||
add_feature_info("Window selection when screen sharing (X11)" ON "XCB-EWMH found. Window selection is enabled when screen sharing (X11).")
|
||||
else()
|
||||
add_feature_info("Window selection when screen sharing (X11)" OFF "XCB-EWMH could not be found on your system. Screen sharing (X11) is limited to the entire screen only. To enable window selection, make sure xcb and xcb-ewmh can be found via pkgconfig.")
|
||||
endif()
|
||||
else()
|
||||
add_feature_info(voip OFF "GStreamer could not be found on your system. As a consequence call support has been disabled. If you don't want that, make sure gstreamer-sdp-1.0>=1.18 gstreamer-webrtc-1.0>=1.18 can be found via pkgconfig.")
|
||||
add_feature_info(voip OFF "GStreamer could not be found on your system. As a consequence call support has been disabled. If you don't want that, make sure gstreamer-sdp-1.0>=1.16 gstreamer-webrtc-1.0>=1.16 can be found via pkgconfig.")
|
||||
endif()
|
||||
|
||||
# single instance functionality
|
||||
@ -483,6 +471,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/dialogs/RawMessage.h
|
||||
src/dialogs/ReCaptcha.h
|
||||
src/dialogs/ReadReceipts.h
|
||||
src/dialogs/RoomSettings.h
|
||||
|
||||
# Emoji
|
||||
src/emoji/EmojiModel.h
|
||||
@ -495,7 +484,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/timeline/TimelineViewManager.h
|
||||
src/timeline/TimelineModel.h
|
||||
src/timeline/DelegateChooser.h
|
||||
src/timeline/Permissions.h
|
||||
|
||||
# UI components
|
||||
src/ui/Avatar.h
|
||||
@ -506,7 +494,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/ui/Label.h
|
||||
src/ui/FloatingButton.h
|
||||
src/ui/Menu.h
|
||||
src/ui/NhekoCursorShape.h
|
||||
src/ui/NhekoDropArea.h
|
||||
src/ui/OverlayWidget.h
|
||||
src/ui/SnackBar.h
|
||||
@ -519,18 +506,14 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/ui/Theme.h
|
||||
src/ui/ThemeManager.h
|
||||
src/ui/UserProfile.h
|
||||
src/ui/RoomSettings.h
|
||||
|
||||
src/notifications/Manager.h
|
||||
|
||||
src/AvatarProvider.h
|
||||
src/BlurhashProvider.h
|
||||
src/Cache_p.h
|
||||
src/CacheCryptoStructs.h
|
||||
src/CallDevices.h
|
||||
src/CallManager.h
|
||||
src/ChatPage.h
|
||||
src/Clipboard.h
|
||||
src/CommunitiesList.h
|
||||
src/CommunitiesListItem.h
|
||||
src/CompletionProxyModel.h
|
||||
@ -539,6 +522,7 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/LoginPage.h
|
||||
src/MainWindow.h
|
||||
src/MxcImageProvider.h
|
||||
src/QuickSwitcher.h
|
||||
src/RegisterPage.h
|
||||
src/RoomInfoListItem.h
|
||||
src/RoomList.h
|
||||
@ -549,7 +533,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
||||
src/UserInfoWidget.h
|
||||
src/UserSettingsPage.h
|
||||
src/UsersModel.h
|
||||
src/RoomsModel.h
|
||||
src/WebRTCSession.h
|
||||
src/WelcomePage.h
|
||||
src/popups/PopupItem.h
|
||||
@ -565,7 +548,7 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
|
||||
|
||||
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/notifications/ManagerMac.cpp src/emoji/MacHelper.mm)
|
||||
set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm)
|
||||
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
|
||||
set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
||||
endif()
|
||||
@ -608,9 +591,6 @@ if(APPLE)
|
||||
elseif(WIN32)
|
||||
target_compile_definitions(nheko PRIVATE WIN32_LEAN_AND_MEAN)
|
||||
target_link_libraries (nheko PRIVATE ${NTDLIB} Qt5::WinMain)
|
||||
if(MSVC)
|
||||
target_compile_options(nheko PUBLIC "/Zc:__cplusplus")
|
||||
endif()
|
||||
else()
|
||||
target_link_libraries (nheko PRIVATE Qt5::DBus)
|
||||
endif()
|
||||
@ -639,6 +619,7 @@ target_link_libraries(nheko PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
lmdbxx::lmdbxx
|
||||
liblmdb::lmdb
|
||||
tweeny
|
||||
SingleApplication::SingleApplication)
|
||||
|
||||
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
|
||||
@ -652,10 +633,6 @@ endif()
|
||||
if (TARGET PkgConfig::GSTREAMER)
|
||||
target_link_libraries(nheko PRIVATE PkgConfig::GSTREAMER)
|
||||
target_compile_definitions(nheko PRIVATE GSTREAMER_AVAILABLE)
|
||||
if (TARGET PkgConfig::XCB)
|
||||
target_link_libraries(nheko PRIVATE PkgConfig::XCB)
|
||||
target_compile_definitions(nheko PRIVATE XCB_AVAILABLE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
@ -668,7 +645,7 @@ if(QML_DEBUGGING)
|
||||
endif()
|
||||
|
||||
|
||||
if(NOT MSVC AND NOT HAIKU)
|
||||
if(NOT MSVC)
|
||||
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR CI_BUILD)
|
||||
target_compile_options(nheko PRIVATE "-Werror")
|
||||
endif()
|
||||
|
3
Makefile
3
Makefile
@ -41,9 +41,6 @@ macos-app-install:
|
||||
lint:
|
||||
./.ci/format.sh
|
||||
|
||||
license:
|
||||
./.ci/licenses.sh
|
||||
|
||||
image:
|
||||
docker build -t nheko-app-image .
|
||||
|
||||
|
38
README.md
38
README.md
@ -2,9 +2,8 @@ nheko
|
||||
----
|
||||
[![Build Status](https://nheko.im/nheko-reborn/nheko/badges/master/pipeline.svg)](https://nheko.im/nheko-reborn/nheko/-/pipelines/latest)
|
||||
[![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.8.2-RC)
|
||||
[![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.8.0)
|
||||
[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/)
|
||||
<a href='https://flatpak.neko.dev/repo/nightly/appstream/io.github.NhekoReborn.Nheko.flatpakref' download='nheko-nightly.flatpakref'><img alt='Download Nightly Flatpak' src='https://img.shields.io/badge/download-flatpak--nightly-green'/></a>
|
||||
[![#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>
|
||||
@ -50,7 +49,7 @@ Specifically there is support for:
|
||||
### Releases
|
||||
|
||||
Releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer)
|
||||
can be found in the [GitHub releases](https://github.com/Nheko-Reborn/nheko/releases).
|
||||
can be found in the [Github releases](https://github.com/Nheko-Reborn/nheko/releases).
|
||||
|
||||
### Repositories
|
||||
|
||||
@ -75,7 +74,7 @@ sudo dnf install nheko
|
||||
|
||||
#### Gentoo Linux
|
||||
```bash
|
||||
sudo eselect repository enable guru
|
||||
sudo eselect repository enable matrix
|
||||
sudo emerge -a nheko
|
||||
```
|
||||
|
||||
@ -115,29 +114,14 @@ with [homebrew](https://brew.sh/):
|
||||
brew install --cask nheko
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
with [Chocolatey](https://chocolatey.org/):
|
||||
|
||||
```posh
|
||||
choco install nheko-reborn
|
||||
```
|
||||
|
||||
### FAQ
|
||||
|
||||
##
|
||||
**Q:** Why don't videos run for me on Windows?
|
||||
|
||||
**A:** You're probably missing the required video codecs, download [K-Lite Codec Pack](https://codecguide.com/download_kl.htm).
|
||||
##
|
||||
|
||||
### Build Requirements
|
||||
|
||||
- Qt5 (5.12 or greater). Required for overlapping hover handlers in Qml.
|
||||
- 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/)
|
||||
- [lmdb++](https://github.com/hoytech/lmdbxx)
|
||||
- [cmark](https://github.com/commonmark/cmark) 0.29 or greater.
|
||||
- Boost 1.70 or greater.
|
||||
- [libolm](https://gitlab.matrix.org/matrix-org/olm)
|
||||
@ -158,7 +142,7 @@ choco install nheko-reborn
|
||||
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 by passing various `-DUSE_BUNDLED_*` flags. By default all dependencies are bundled *if* you enable hunter. (The exception to that is OpenSSL, which is always disabled by default.)
|
||||
You can select which bundled dependencies you want to use by passing various `-DUSE_BUNDLED_*` flags. By default all dependencies are bundled *if* you enable hunter.
|
||||
If you experience build issues and you are trying to link `mtxclient` library without hunter, make sure the library version(commit) as mentioned in the `CMakeList.txt` is used. Sometimes we have to make breaking changes in `mtxclient` and for that period the master branch of both repos may not be compatible.
|
||||
|
||||
The bundle flags are currently:
|
||||
@ -175,8 +159,6 @@ The bundle flags are currently:
|
||||
- USE_BUNDLED_LMDBXX
|
||||
- USE_BUNDLED_TWEENY
|
||||
|
||||
A note on bundled OpenSSL: You need to explicitly enable it and it will not be using your system certificate directory by default, if you enable it. You need to override that at runtime with the SSL_CERT_FILE variable. On Windows it will still be using your system certificates though, since it loads them from the system store instead of the OpenSSL directory.
|
||||
|
||||
#### Linux
|
||||
|
||||
If you don't want to install any external dependencies, you can generate an AppImage locally using docker. It is not that well maintained though...
|
||||
@ -211,7 +193,7 @@ sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig dev-libs/qtkeychain
|
||||
|
||||
```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++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev
|
||||
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,graphicaleffects,quick-controls2} qt5keychain-dev
|
||||
```
|
||||
This will install all dependencies, except for tweeny (use bundled tweeny)
|
||||
and mtxclient (needs to be build separately).
|
||||
@ -223,8 +205,8 @@ and mtxclient (needs to be build separately).
|
||||
```bash
|
||||
sudo apt install cmake gcc make automake liblmdb-dev \
|
||||
qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \
|
||||
qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev \
|
||||
qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform\
|
||||
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 \
|
||||
qt5keychain-dev
|
||||
```
|
||||
|
||||
|
31
appveyor.yml
31
appveyor.yml
@ -15,7 +15,7 @@ environment:
|
||||
|
||||
|
||||
cache:
|
||||
- c:\hunter\ -> appveyor.yml
|
||||
- c:\hunter\
|
||||
- build\_deps -> appveyor.yml
|
||||
|
||||
build:
|
||||
@ -23,16 +23,15 @@ build:
|
||||
|
||||
install:
|
||||
- set QT_DIR=C:\Qt\5.15\msvc2019_64
|
||||
- set PATH=Path=C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin;C:\Program Files\Git\cmd;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\PowerShell\7\;C:\Program Files\7-Zip;C:\Program Files\Microsoft\Web Platform Installer\;C:\Tools\NuGet;C:\Tools\PsTools;C:\Program Files\Git\usr\bin;C:\Program Files\Git LFS;C:\Program Files\Mercurial\;C:\Program Files (x86)\Subversion\bin;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files\dotnet\;C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\Extensions\Microsoft\SQLDB\DAC\150;C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin;C:\Tools\xUnit;C:\Tools\xUnit20;C:\Tools\NUnit\bin;C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\Extensions\TestPlatform;C:\Ruby193\bin;C:\Tools\WebDriver;C:\Python27;C:\Python27\Scripts;C:\Program Files\erl10.7\bin;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files (x86)\Microsoft DirectX SDK;C:\Program Files\Microsoft Service Fabric\bin\Fabric\Fabric.Code;C:\Program Files\Microsoft SDKs\Service Fabric\Tools\ServiceFabricLocalClusterManager;C:\Tools\Doxygen;C:\Program Files (x86)\CMake\bin;C:\ProgramData\chocolatey\bin;C:\Tools\vcpkg;C:\Tools\Coverity\bin;C:\Program Files (x86)\NSIS;C:\Tools\Octopus;C:\Program Files\Meson\;C:\Tools\GitVersion;C:\Tools\NUnit3\bin;C:\Users\appveyor\AppData\Local\Microsoft\WindowsApps;C:\Users\appveyor\.dotnet\tools;C:\Program Files\AppVeyor\BuildAgent\
|
||||
- set PATH=C:\Strawberry\perl\bin;C:\Python39-x64;%QT_DIR%\bin;%PATH%
|
||||
- set PATH=C:\Strawberry\perl\bin;%PATH%;%QT_DIR%\bin
|
||||
- call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
|
||||
|
||||
build_script:
|
||||
# VERSION format: branch-master/branch-1.2
|
||||
# INSTVERSION format: x.y.z
|
||||
# WINVERSION format: 9999.0.0.123/1.2.0.234
|
||||
- if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.8.2
|
||||
- if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.8.2
|
||||
- if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.8.0
|
||||
- if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.8.0
|
||||
- if "%APPVEYOR_REPO_TAG%"=="false" if "%APPVEYOR_REPO_BRANCH%"=="master" set INSTVERSION=9999.0
|
||||
- if "%APPVEYOR_REPO_TAG%"=="false" set WINVERSION=%INSTVERSION%.0.%APPVEYOR_BUILD_NUMBER%
|
||||
# VERSION format: v1.2.3/v1.3.4
|
||||
@ -47,9 +46,10 @@ build_script:
|
||||
- echo %DATE%
|
||||
|
||||
# Build nheko
|
||||
#- cmake -G "Visual Studio 15 2017 Win64" -H. -Bbuild
|
||||
- cmake -G "Visual Studio 16 2019" -A x64 -H. -Bbuild
|
||||
-DHUNTER_ROOT="C:\hunter"
|
||||
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON
|
||||
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF
|
||||
-DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Release
|
||||
|
||||
- cmake --build build --config Release
|
||||
@ -61,7 +61,7 @@ after_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 resources\qml\ NhekoRelease\nheko.exe
|
||||
- windeployqt --qmldir %QT_DIR%\qml\ --release NhekoRelease\nheko.exe
|
||||
|
||||
- 7z a nheko_win_64.zip .\NhekoRelease\*
|
||||
- ls -lh build\Release\
|
||||
@ -77,19 +77,25 @@ after_build:
|
||||
- mkdir installer\packages\io.github.nhekoreborn.nheko
|
||||
- mkdir installer\packages\io.github.nhekoreborn.nheko\data
|
||||
- mkdir installer\packages\io.github.nhekoreborn.nheko\meta
|
||||
- mkdir installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
|
||||
# Copy installer data
|
||||
- copy %BUILD%\resources\nheko.ico installer\config
|
||||
- copy %BUILD%\resources\nheko.png installer\config
|
||||
- copy %BUILD%\COPYING installer\packages\io.github.nhekoreborn.nheko\meta\license.txt
|
||||
- copy %BUILD%\COPYING installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\license.txt
|
||||
- copy %BUILD%\deploy\installer\config.xml installer\config
|
||||
- copy %BUILD%\deploy\installer\controlscript.qs installer\config
|
||||
- copy %BUILD%\deploy\installer\uninstall.qs installer\packages\io.github.nhekoreborn.nheko\data
|
||||
- copy %BUILD%\deploy\installer\gui\package.xml installer\packages\io.github.nhekoreborn.nheko\meta
|
||||
- copy %BUILD%\deploy\installer\gui\installscript.qs installer\packages\io.github.nhekoreborn.nheko\meta
|
||||
- copy %BUILD%\deploy\installer\cleanup\package.xml installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
|
||||
- copy %BUILD%\deploy\installer\cleanup\installscript.qs installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
|
||||
# Amend version and date
|
||||
- sed -i "s/__VERSION__/0.8.2/" installer\config\config.xml
|
||||
- sed -i "s/__VERSION__/0.8.2/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
|
||||
- sed -i "s/__VERSION__/0.8.0/" installer\config\config.xml
|
||||
- sed -i "s/__VERSION__/0.8.0/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
|
||||
- sed -i "s/__VERSION__/0.8.0/" installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\package.xml
|
||||
- sed -i "s/__DATE__/%DATE%/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
|
||||
- sed -i "s/__DATE__/%DATE%/" installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\package.xml
|
||||
# Copy nheko data
|
||||
- xcopy NhekoData\*.* installer\packages\io.github.nhekoreborn.nheko\data\*.* /s /e /c /y
|
||||
- move NhekoRelease\nheko.exe installer\packages\io.github.nhekoreborn.nheko\data
|
||||
@ -101,13 +107,13 @@ after_build:
|
||||
- set PATH=%BUILD%\tools\bin;%PATH%
|
||||
- binarycreator.exe -f -c installer\config\config.xml -p installer\packages nheko-installer.exe
|
||||
|
||||
- copy nheko-installer.exe nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe
|
||||
- copy nheko-installer.exe nheko-%APPVEYOR_PULL_REQUEST_HEAD_COMMIT%-installer.exe
|
||||
- ps: .\.ci\upload-nightly.ps1
|
||||
- mv nheko-installer.exe nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe
|
||||
|
||||
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%/)
|
||||
|
||||
before_deploy:
|
||||
- ps: .\.ci\upload-nightly.ps1
|
||||
deploy:
|
||||
- description: "Development builds"
|
||||
provider: GitHub
|
||||
@ -122,4 +128,3 @@ deploy:
|
||||
artifacts:
|
||||
- path: nheko_win_64.zip
|
||||
- path: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe
|
||||
- path: nheko-$(APPVEYOR_PULL_REQUEST_HEAD_COMMIT)-installer.exe
|
||||
|
28
deploy/installer/cleanup/installscript.qs
Normal file
28
deploy/installer/cleanup/installscript.qs
Normal file
@ -0,0 +1,28 @@
|
||||
function Component()
|
||||
{
|
||||
}
|
||||
|
||||
Component.prototype.createOperations = function()
|
||||
{
|
||||
component.createOperations();
|
||||
|
||||
try
|
||||
{
|
||||
if( installer.value("os") === "win" )
|
||||
{
|
||||
/**
|
||||
* Cleanup AppData and registry
|
||||
*/
|
||||
component.addElevatedOperation("Execute","UNDOEXECUTE","cmd /C reg delete HKEY_CURRENT_USER\Software\nheko\nheko /f");
|
||||
var localappdata = installer.environmentVariable("LOCALAPPDATA");
|
||||
if( localappdata != "" )
|
||||
{
|
||||
component.addElevatedOperation("Execute","UNDOEXECUTE","cmd /C rmdir "+localappdata+"\nheko /f");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( e )
|
||||
{
|
||||
print( e );
|
||||
}
|
||||
}
|
10
deploy/installer/cleanup/package.xml
Normal file
10
deploy/installer/cleanup/package.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Package>
|
||||
<DisplayName>Cleanup AppData and Registry</DisplayName>
|
||||
<Description>Cleans up AppData and Registry when selected (logs you out) - Broken</Description>
|
||||
<Version>__VERSION__</Version>
|
||||
<ReleaseDate>__DATE__</ReleaseDate>
|
||||
<SortingPriority>80</SortingPriority>
|
||||
<Script>installscript.qs</Script>
|
||||
<Checkable>false</Checkable>
|
||||
</Package>
|
@ -23,18 +23,6 @@ Component.prototype.createOperations = function()
|
||||
component.addOperation( "CreateShortcut", "@TargetDir@\\nheko.exe", "@DesktopDir@\\nheko.lnk",
|
||||
"workingDirectory=@TargetDir@", "iconPath=@TargetDir@\\nheko.exe",
|
||||
"iconId=0", "description=Desktop client for the Matrix protocol");
|
||||
|
||||
var reg = installer.environmentVariable("SystemRoot") + "\\System32\\reg.exe";
|
||||
var key= "HKEY_CLASSES_ROOT\\matrix";
|
||||
component.addOperation("Execute", reg, "ADD", key, "/f", "/t", "REG_SZ", "/d", "URL:Matrix Protocol");
|
||||
component.addOperation("Execute", reg, "ADD", key, "/f", "/v", "URL Protocol", "/t", "REG_SZ");
|
||||
var iconkey = "HKEY_CLASSES_ROOT\\matrix\\DefaultIcon";
|
||||
component.addOperation("Execute", reg, "ADD", iconkey, "/f", "/t", "REG_SZ", "/d", "@TargetDir@\\nheko.exe,1");
|
||||
component.addOperation("Execute", reg, "ADD", "HKEY_CLASSES_ROOT\\matrix\\shell", "/f");
|
||||
component.addOperation("Execute", reg, "ADD", "HKEY_CLASSES_ROOT\\matrix\\shell\\open", "/f");
|
||||
var commandkey = "HKEY_CLASSES_ROOT\\matrix\\shell\\open\\command"
|
||||
component.addOperation("Execute", reg, "ADD", commandkey, "/f");
|
||||
component.addOperation("Execute", reg, "ADD", commandkey, "/f", "/t", "REG_SZ", "/d", "\"@TargetDir@\\nheko.exe\" \"%1\"");
|
||||
}
|
||||
}
|
||||
catch( e )
|
||||
|
269
io.github.NhekoReborn.Nheko.json
Normal file
269
io.github.NhekoReborn.Nheko.json
Normal file
@ -0,0 +1,269 @@
|
||||
{
|
||||
"id": "io.github.NhekoReborn.Nheko",
|
||||
"command": "nheko",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "5.15",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"rename-icon": "nheko",
|
||||
"rename-desktop-file": "nheko.desktop",
|
||||
"rename-appdata-file": "nheko.appdata.xml",
|
||||
"finish-args": [
|
||||
"--device=dri",
|
||||
"--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": "5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb",
|
||||
"type": "archive",
|
||||
"url": "https://github.com/gabime/spdlog/archive/v1.8.1.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": [
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DBUILD_TEST_APPLICATION=OFF",
|
||||
"-DQTKEYCHAIN_STATIC=ON"
|
||||
],
|
||||
"buildsystem": "cmake-ninja",
|
||||
"name": "QtKeychain",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "815fe610353ff8ad7e2f1121c368a74df8db5eb7",
|
||||
"tag": "v0.12.0",
|
||||
"type": "git",
|
||||
"url": "https://github.com/frankosterfeld/qtkeychain.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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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://sourceforge.net/projects/boost/files/boost/1.72.0/boost_1_72_0.tar.bz2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"buildsystem": "meson",
|
||||
"name": "gstreamer",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "a42fe476d3ee5576921f67a331464065ec33b9a4",
|
||||
"tag": "1.18.3",
|
||||
"type": "git",
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"config-opts": [
|
||||
"-Dcompositor=enabled",
|
||||
"-Dgl=enabled"
|
||||
],
|
||||
"buildsystem": "meson",
|
||||
"name": "gstreamer-plugins-base",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "2cc319ee13f6b72df3d432b7c75aca81feb260e5",
|
||||
"tag": "1.18.3",
|
||||
"type": "git",
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"config-opts": [
|
||||
"-Dpulse=enabled",
|
||||
"-Dqt5=enabled",
|
||||
"-Drtp=enabled",
|
||||
"-Drtpmanager=enabled",
|
||||
"-Dvpx=enabled"
|
||||
],
|
||||
"buildsystem": "meson",
|
||||
"name": "gstreamer-plugins-good",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "e816c6cd73c9e0676828c9e227a049ebad3d019f",
|
||||
"tag": "1.18.3",
|
||||
"type": "git",
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"config-opts": [
|
||||
"-Ddtls=enabled",
|
||||
"-Dgl=enabled",
|
||||
"-Dopenh264=enabled",
|
||||
"-Dopus=enabled",
|
||||
"-Dsrtp=enabled",
|
||||
"-Dwebrtc=enabled",
|
||||
"-Dflite=disabled"
|
||||
],
|
||||
"buildsystem": "meson",
|
||||
"name": "gstreamer-plugins-bad",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "382e373d9be363f1e21b12990a4d12f1ecb6df41",
|
||||
"tag": "1.18.3",
|
||||
"type": "git",
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"config-opts": [
|
||||
"-DBUILD_LIB_TESTS=OFF",
|
||||
"-DBUILD_LIB_EXAMPLES=OFF",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DBUILD_SHARED_LIBS=OFF"
|
||||
],
|
||||
"buildsystem": "cmake-ninja",
|
||||
"name": "mtxclient",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "2d6e3f79917ce2065a54ca32e6a9f9d42c0b6347",
|
||||
"tag": "v0.4.0",
|
||||
"type": "git",
|
||||
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"-DCOMPILE_QML=ON"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
id: io.github.NhekoReborn.Nheko
|
||||
command: nheko
|
||||
runtime: org.kde.Platform
|
||||
runtime-version: '5.15'
|
||||
sdk: org.kde.Sdk
|
||||
rename-icon: nheko
|
||||
rename-desktop-file: nheko.desktop
|
||||
rename-appdata-file: nheko.appdata.xml
|
||||
finish-args:
|
||||
- --device=dri
|
||||
# needed for webcams, see #517
|
||||
- --device=all
|
||||
- --share=ipc
|
||||
- --share=network
|
||||
- --socket=pulseaudio
|
||||
- --socket=wayland
|
||||
- --socket=x11
|
||||
- --talk-name=org.freedesktop.Notifications
|
||||
- --talk-name=org.freedesktop.secrets
|
||||
- --talk-name=org.freedesktop.StatusNotifierItem
|
||||
- --talk-name=org.kde.*
|
||||
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: 5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb
|
||||
type: archive
|
||||
url: https://github.com/gabime/spdlog/archive/v1.8.1.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:
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DBUILD_TEST_APPLICATION=OFF
|
||||
- -DQTKEYCHAIN_STATIC=ON
|
||||
buildsystem: cmake-ninja
|
||||
name: QtKeychain
|
||||
sources:
|
||||
- commit: 815fe610353ff8ad7e2f1121c368a74df8db5eb7
|
||||
tag: v0.12.0
|
||||
type: git
|
||||
url: https://github.com/frankosterfeld/qtkeychain.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
|
||||
- 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://sourceforge.net/projects/boost/files/boost/1.72.0/boost_1_72_0.tar.bz2
|
||||
- buildsystem: meson
|
||||
name: gstreamer
|
||||
sources:
|
||||
- commit: a42fe476d3ee5576921f67a331464065ec33b9a4
|
||||
tag: 1.18.3
|
||||
type: git
|
||||
url: https://gitlab.freedesktop.org/gstreamer/gstreamer.git
|
||||
- config-opts:
|
||||
- -Dcompositor=enabled
|
||||
- -Dgl=enabled
|
||||
buildsystem: meson
|
||||
name: gstreamer-plugins-base
|
||||
sources:
|
||||
- commit: 2cc319ee13f6b72df3d432b7c75aca81feb260e5
|
||||
tag: 1.18.3
|
||||
type: git
|
||||
url: https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git
|
||||
- config-opts:
|
||||
- -Dpulse=enabled
|
||||
- -Dqt5=enabled
|
||||
- -Drtp=enabled
|
||||
- -Drtpmanager=enabled
|
||||
- -Dvpx=enabled
|
||||
buildsystem: meson
|
||||
name: gstreamer-plugins-good
|
||||
sources:
|
||||
- commit: e816c6cd73c9e0676828c9e227a049ebad3d019f
|
||||
tag: 1.18.3
|
||||
type: git
|
||||
url: https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git
|
||||
- config-opts:
|
||||
- -Ddtls=enabled
|
||||
- -Dgl=enabled
|
||||
- -Dopenh264=enabled
|
||||
- -Dopus=enabled
|
||||
- -Dsrtp=enabled
|
||||
- -Dwebrtc=enabled
|
||||
- -Dflite=disabled
|
||||
buildsystem: meson
|
||||
name: gstreamer-plugins-bad
|
||||
sources:
|
||||
- commit: 382e373d9be363f1e21b12990a4d12f1ecb6df41
|
||||
tag: 1.18.3
|
||||
type: git
|
||||
url: https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git
|
||||
- config-opts:
|
||||
- -DBUILD_LIB_TESTS=OFF
|
||||
- -DBUILD_LIB_EXAMPLES=OFF
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DBUILD_SHARED_LIBS=OFF
|
||||
buildsystem: cmake-ninja
|
||||
name: mtxclient
|
||||
sources:
|
||||
- commit: 5d2f055ea9403770039ddf66b1900f890cd5cde7
|
||||
type: git
|
||||
url: https://github.com/Nheko-Reborn/mtxclient.git
|
||||
- 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
|
||||
- -DCOMPILE_QML=ON
|
||||
buildsystem: cmake-ninja
|
||||
name: nheko
|
||||
sources:
|
||||
- path: .
|
||||
type: dir
|
||||
skip:
|
||||
- build-flatpak
|
||||
- dest: .deps/lmdbxx
|
||||
sha256: 5e12eb3aefe9050068af7df2c663edabc977ef34c9e7ba7b9d2c43e0ad47d8df
|
||||
type: archive
|
||||
url: https://github.com/hoytech/lmdbxx/archive/1.0.0.tar.gz
|
@ -1,10 +0,0 @@
|
||||
[Flatpak Ref]
|
||||
Title=Nheko Nightly
|
||||
Name=io.github.NhekoReborn.Nheko
|
||||
Branch=master
|
||||
Url=https://flatpak.neko.dev/repo/nightly
|
||||
Homepage=https://nheko-reborn.github.io/
|
||||
Icon=https://nheko.im/nheko-reborn/nheko/-/raw/master/resources/nheko.svg
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
IsRuntime=false
|
||||
GPGKey=mDMEXENMphYJKwYBBAHaRw8BAQdAqn+Eo42lPoGpJ5HaOf4nFGfxR0QtOggJTCfsdbOyL4e0Kk5pY29sYXMgV2VybmVyIDxuaWNvbGFzLndlcm5lckBob3RtYWlsLmRlPoiWBBMWCAA+FiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAlxDTVUCGwMFCQtJjooFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQkgauGyMeBbs2rQD/dAEoOGT21BL85A8LmPK743EboBAjoRbWcI1hHnvS28AA/3b3HYGwgvTC6hQLyz75zjpeO5ZaUtbezRyDUR4xabMAtCROaWNvbGFzIFdlcm5lciA8bmljb2xhc0BuZWtvZGV2Lm5ldD6IlgQTFggAPhYhBNWLRiQlpqNxJcb+25IGrhsjHgW7BQJcQ01GAhsDBQkLSY6KBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEJIGrhsjHgW7GxwBANT4gL03Uu9N5KmTBihC7B71+0r7M/azPbUh86NthCeIAQCF2JXa0axBKhgQF5fWC5ncL+m8ZpH7C5rzDqVgO82WALQnTmljb2xhcyBXZXJuZXIgPG5pY29sYXMud2VybmVyQGdteC5uZXQ+iJYEExYIAD4WIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbAwUJC0mOigULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCSBq4bIx4FuxU5APoCRDYlJW0oTsJs3lcTTB5Nsqb3X4iCEDCjIgsA3wtsIwEAlGBzD8ElCYi2+8m8esSRNlmpRcGoqgXbceLxPUXFpQu4OARcQ0ymEgorBgEEAZdVAQUBAQdAD8dBmT3iqrqdlxSw90L0SIH11fVxiX9MdWfBkTi6PzUDAQgHiH4EGBYIACYWIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbDAUJC0mOigAKCRCSBq4bIx4Fu/LNAQDhH64IBic6h7H3uvtSAFT4xNn7Epobt2baIaDp7uKsQQEAyI+oc5dLknABwIOMrQQuZCmGejx9e4/8HEqLCdszhgG4MwRgNICHFgkrBgEEAdpHDwEBB0DR9eFFzfR62FIi7g+txcQenLvKFzhlyTz0wo3icOy6RYj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0gIcCGwIFCQlmAYAAgQkQkgauGyMeBbt2IAQZFggAHRYhBGz14re9h4cNPaFEKMjXXmEHc/LZBQJgNICHAAoJEMjXXmEHc/LZhVMBAPdYRspdeFh6E9BDxGubT705e/pZFdCHjCToDyxgdW5KAP9sU0hFI5VDHD1h98RzxSt7hc3jxyPSzbG1MBUJ9gbfCVhcAPsFfeZc3v5UBgmn4uICFEGjlzAWCQ7WctE6QTSkY5aL/wD9ETJH5lB+i/8km/sOBKQozXR0yHHw46gB6ZWMeN1wfgq4MwRgNPutFgkrBgEEAdpHDwEBB0APwMn0FJmnAds8IO8iCl/RHr7fz8xnpGd7E4zVgCNZpIj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0+60CGwIFCQANLwAAgQkQkgauGyMeBbt2IAQZFggAHRYhBAH7QBkzNfVIZJM93RNnXzGtBKQcBQJgNPutAAoJEBNnXzGtBKQcHnUA/0E2H5sxmfZ+EWFTso3X4NWu3uN2xF+MdNaY8C72f9H6AP91XaNmlB9gV61rg6wcB5E/j0998yWS9gltY1XY1ImqDPvlAP4sHFs5zuDazgKYxZ/kFhENCgEStdpnvJjt/DxmQPVT3AD/QK5vGoMTIeYjihv0QCnnRDfboTTZHlaEqJW8i02PQww=
|
@ -1,8 +0,0 @@
|
||||
[Flatpak Repo]
|
||||
Title=Nheko Nightly
|
||||
Url=https://flatpak.neko.dev/repo/nightly
|
||||
Homepage=https://nheko.im/
|
||||
Comment=Nheko nightly release repository
|
||||
Description=Nheko nightly release repository
|
||||
Icon=https://nheko.im/nheko-reborn/nheko/-/raw/master/resources/nheko.svg
|
||||
GPGKey=mDMEXENMphYJKwYBBAHaRw8BAQdAqn+Eo42lPoGpJ5HaOf4nFGfxR0QtOggJTCfsdbOyL4e0Kk5pY29sYXMgV2VybmVyIDxuaWNvbGFzLndlcm5lckBob3RtYWlsLmRlPoiWBBMWCAA+FiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAlxDTVUCGwMFCQtJjooFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQkgauGyMeBbs2rQD/dAEoOGT21BL85A8LmPK743EboBAjoRbWcI1hHnvS28AA/3b3HYGwgvTC6hQLyz75zjpeO5ZaUtbezRyDUR4xabMAtCROaWNvbGFzIFdlcm5lciA8bmljb2xhc0BuZWtvZGV2Lm5ldD6IlgQTFggAPhYhBNWLRiQlpqNxJcb+25IGrhsjHgW7BQJcQ01GAhsDBQkLSY6KBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEJIGrhsjHgW7GxwBANT4gL03Uu9N5KmTBihC7B71+0r7M/azPbUh86NthCeIAQCF2JXa0axBKhgQF5fWC5ncL+m8ZpH7C5rzDqVgO82WALQnTmljb2xhcyBXZXJuZXIgPG5pY29sYXMud2VybmVyQGdteC5uZXQ+iJYEExYIAD4WIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbAwUJC0mOigULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCSBq4bIx4FuxU5APoCRDYlJW0oTsJs3lcTTB5Nsqb3X4iCEDCjIgsA3wtsIwEAlGBzD8ElCYi2+8m8esSRNlmpRcGoqgXbceLxPUXFpQu4OARcQ0ymEgorBgEEAZdVAQUBAQdAD8dBmT3iqrqdlxSw90L0SIH11fVxiX9MdWfBkTi6PzUDAQgHiH4EGBYIACYWIQTVi0YkJaajcSXG/tuSBq4bIx4FuwUCXENMpgIbDAUJC0mOigAKCRCSBq4bIx4Fu/LNAQDhH64IBic6h7H3uvtSAFT4xNn7Epobt2baIaDp7uKsQQEAyI+oc5dLknABwIOMrQQuZCmGejx9e4/8HEqLCdszhgG4MwRgNICHFgkrBgEEAdpHDwEBB0DR9eFFzfR62FIi7g+txcQenLvKFzhlyTz0wo3icOy6RYj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0gIcCGwIFCQlmAYAAgQkQkgauGyMeBbt2IAQZFggAHRYhBGz14re9h4cNPaFEKMjXXmEHc/LZBQJgNICHAAoJEMjXXmEHc/LZhVMBAPdYRspdeFh6E9BDxGubT705e/pZFdCHjCToDyxgdW5KAP9sU0hFI5VDHD1h98RzxSt7hc3jxyPSzbG1MBUJ9gbfCVhcAPsFfeZc3v5UBgmn4uICFEGjlzAWCQ7WctE6QTSkY5aL/wD9ETJH5lB+i/8km/sOBKQozXR0yHHw46gB6ZWMeN1wfgq4MwRgNPutFgkrBgEEAdpHDwEBB0APwMn0FJmnAds8IO8iCl/RHr7fz8xnpGd7E4zVgCNZpIj1BBgWCAAmFiEE1YtGJCWmo3Elxv7bkgauGyMeBbsFAmA0+60CGwIFCQANLwAAgQkQkgauGyMeBbt2IAQZFggAHRYhBAH7QBkzNfVIZJM93RNnXzGtBKQcBQJgNPutAAoJEBNnXzGtBKQcHnUA/0E2H5sxmfZ+EWFTso3X4NWu3uN2xF+MdNaY8C72f9H6AP91XaNmlB9gV61rg6wcB5E/j0998yWS9gltY1XY1ImqDPvlAP4sHFs5zuDazgKYxZ/kFhENCgEStdpnvJjt/DxmQPVT3AD/QK5vGoMTIeYjihv0QCnnRDfboTTZHlaEqJW8i02PQww=
|
@ -1,11 +1,11 @@
|
||||
# emoji-test.txt
|
||||
# Date: 2020-09-12, 22:19:50 GMT
|
||||
# © 2020 Unicode®, Inc.
|
||||
# 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.1
|
||||
# Version: 13.0
|
||||
#
|
||||
# For documentation and usage, see http://www.unicode.org/reports/tr51
|
||||
#
|
||||
@ -79,13 +79,10 @@
|
||||
1F610 ; fully-qualified # 😐 E0.7 neutral face
|
||||
1F611 ; fully-qualified # 😑 E1.0 expressionless face
|
||||
1F636 ; fully-qualified # 😶 E1.0 face without mouth
|
||||
1F636 200D 1F32B FE0F ; fully-qualified # 😶🌫️ E13.1 face in clouds
|
||||
1F636 200D 1F32B ; minimally-qualified # 😶🌫 E13.1 face in clouds
|
||||
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
|
||||
1F62E 200D 1F4A8 ; fully-qualified # 😮💨 E13.1 face exhaling
|
||||
1F925 ; fully-qualified # 🤥 E3.0 lying face
|
||||
|
||||
# subgroup: face-sleepy
|
||||
@ -105,8 +102,7 @@
|
||||
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 knocked-out face
|
||||
1F635 200D 1F4AB ; fully-qualified # 😵💫 E13.1 face with spiral eyes
|
||||
1F635 ; fully-qualified # 😵 E0.6 dizzy face
|
||||
1F92F ; fully-qualified # 🤯 E5.0 exploding head
|
||||
|
||||
# subgroup: face-hat
|
||||
@ -197,10 +193,6 @@
|
||||
2763 FE0F ; fully-qualified # ❣️ E1.0 heart exclamation
|
||||
2763 ; unqualified # ❣ E1.0 heart exclamation
|
||||
1F494 ; fully-qualified # 💔 E0.6 broken heart
|
||||
2764 FE0F 200D 1F525 ; fully-qualified # ❤️🔥 E13.1 heart on fire
|
||||
2764 200D 1F525 ; unqualified # ❤🔥 E13.1 heart on fire
|
||||
2764 FE0F 200D 1FA79 ; fully-qualified # ❤️🩹 E13.1 mending heart
|
||||
2764 200D 1FA79 ; unqualified # ❤🩹 E13.1 mending heart
|
||||
2764 FE0F ; fully-qualified # ❤️ E0.6 red heart
|
||||
2764 ; unqualified # ❤ E0.6 red heart
|
||||
1F9E1 ; fully-qualified # 🧡 E5.0 orange heart
|
||||
@ -232,8 +224,8 @@
|
||||
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
|
||||
1F4A4 ; fully-qualified # 💤 E0.6 zzz
|
||||
|
||||
# Smileys & Emotion subtotal: 170
|
||||
# Smileys & Emotion subtotal: 170 w/o modifiers
|
||||
# Smileys & Emotion subtotal: 162
|
||||
# Smileys & Emotion subtotal: 162 w/o modifiers
|
||||
|
||||
# group: People & Body
|
||||
|
||||
@ -545,36 +537,12 @@
|
||||
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 person: beard
|
||||
1F9D4 1F3FB ; fully-qualified # 🧔🏻 E5.0 person: light skin tone, beard
|
||||
1F9D4 1F3FC ; fully-qualified # 🧔🏼 E5.0 person: medium-light skin tone, beard
|
||||
1F9D4 1F3FD ; fully-qualified # 🧔🏽 E5.0 person: medium skin tone, beard
|
||||
1F9D4 1F3FE ; fully-qualified # 🧔🏾 E5.0 person: medium-dark skin tone, beard
|
||||
1F9D4 1F3FF ; fully-qualified # 🧔🏿 E5.0 person: dark skin tone, beard
|
||||
1F9D4 200D 2642 FE0F ; fully-qualified # 🧔♂️ E13.1 man: beard
|
||||
1F9D4 200D 2642 ; minimally-qualified # 🧔♂ E13.1 man: beard
|
||||
1F9D4 1F3FB 200D 2642 FE0F ; fully-qualified # 🧔🏻♂️ E13.1 man: light skin tone, beard
|
||||
1F9D4 1F3FB 200D 2642 ; minimally-qualified # 🧔🏻♂ E13.1 man: light skin tone, beard
|
||||
1F9D4 1F3FC 200D 2642 FE0F ; fully-qualified # 🧔🏼♂️ E13.1 man: medium-light skin tone, beard
|
||||
1F9D4 1F3FC 200D 2642 ; minimally-qualified # 🧔🏼♂ E13.1 man: medium-light skin tone, beard
|
||||
1F9D4 1F3FD 200D 2642 FE0F ; fully-qualified # 🧔🏽♂️ E13.1 man: medium skin tone, beard
|
||||
1F9D4 1F3FD 200D 2642 ; minimally-qualified # 🧔🏽♂ E13.1 man: medium skin tone, beard
|
||||
1F9D4 1F3FE 200D 2642 FE0F ; fully-qualified # 🧔🏾♂️ E13.1 man: medium-dark skin tone, beard
|
||||
1F9D4 1F3FE 200D 2642 ; minimally-qualified # 🧔🏾♂ E13.1 man: medium-dark skin tone, beard
|
||||
1F9D4 1F3FF 200D 2642 FE0F ; fully-qualified # 🧔🏿♂️ E13.1 man: dark skin tone, beard
|
||||
1F9D4 1F3FF 200D 2642 ; minimally-qualified # 🧔🏿♂ E13.1 man: dark skin tone, beard
|
||||
1F9D4 200D 2640 FE0F ; fully-qualified # 🧔♀️ E13.1 woman: beard
|
||||
1F9D4 200D 2640 ; minimally-qualified # 🧔♀ E13.1 woman: beard
|
||||
1F9D4 1F3FB 200D 2640 FE0F ; fully-qualified # 🧔🏻♀️ E13.1 woman: light skin tone, beard
|
||||
1F9D4 1F3FB 200D 2640 ; minimally-qualified # 🧔🏻♀ E13.1 woman: light skin tone, beard
|
||||
1F9D4 1F3FC 200D 2640 FE0F ; fully-qualified # 🧔🏼♀️ E13.1 woman: medium-light skin tone, beard
|
||||
1F9D4 1F3FC 200D 2640 ; minimally-qualified # 🧔🏼♀ E13.1 woman: medium-light skin tone, beard
|
||||
1F9D4 1F3FD 200D 2640 FE0F ; fully-qualified # 🧔🏽♀️ E13.1 woman: medium skin tone, beard
|
||||
1F9D4 1F3FD 200D 2640 ; minimally-qualified # 🧔🏽♀ E13.1 woman: medium skin tone, beard
|
||||
1F9D4 1F3FE 200D 2640 FE0F ; fully-qualified # 🧔🏾♀️ E13.1 woman: medium-dark skin tone, beard
|
||||
1F9D4 1F3FE 200D 2640 ; minimally-qualified # 🧔🏾♀ E13.1 woman: medium-dark skin tone, beard
|
||||
1F9D4 1F3FF 200D 2640 FE0F ; fully-qualified # 🧔🏿♀️ E13.1 woman: dark skin tone, beard
|
||||
1F9D4 1F3FF 200D 2640 ; minimally-qualified # 🧔🏿♀ E13.1 woman: dark skin tone, beard
|
||||
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
|
||||
@ -1436,12 +1404,6 @@
|
||||
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
|
||||
1F977 ; fully-qualified # 🥷 E13.0 ninja
|
||||
1F977 1F3FB ; fully-qualified # 🥷🏻 E13.0 ninja: light skin tone
|
||||
1F977 1F3FC ; fully-qualified # 🥷🏼 E13.0 ninja: medium-light skin tone
|
||||
1F977 1F3FD ; fully-qualified # 🥷🏽 E13.0 ninja: medium skin tone
|
||||
1F977 1F3FE ; fully-qualified # 🥷🏾 E13.0 ninja: medium-dark skin tone
|
||||
1F977 1F3FF ; fully-qualified # 🥷🏿 E13.0 ninja: 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
|
||||
@ -1514,24 +1476,24 @@
|
||||
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 person with skullcap
|
||||
1F472 1F3FB ; fully-qualified # 👲🏻 E1.0 person with skullcap: light skin tone
|
||||
1F472 1F3FC ; fully-qualified # 👲🏼 E1.0 person with skullcap: medium-light skin tone
|
||||
1F472 1F3FD ; fully-qualified # 👲🏽 E1.0 person with skullcap: medium skin tone
|
||||
1F472 1F3FE ; fully-qualified # 👲🏾 E1.0 person with skullcap: medium-dark skin tone
|
||||
1F472 1F3FF ; fully-qualified # 👲🏿 E1.0 person with skullcap: 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 person in tuxedo
|
||||
1F935 1F3FB ; fully-qualified # 🤵🏻 E3.0 person in tuxedo: light skin tone
|
||||
1F935 1F3FC ; fully-qualified # 🤵🏼 E3.0 person in tuxedo: medium-light skin tone
|
||||
1F935 1F3FD ; fully-qualified # 🤵🏽 E3.0 person in tuxedo: medium skin tone
|
||||
1F935 1F3FE ; fully-qualified # 🤵🏾 E3.0 person in tuxedo: medium-dark skin tone
|
||||
1F935 1F3FF ; fully-qualified # 🤵🏿 E3.0 person in tuxedo: 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
|
||||
@ -1556,12 +1518,12 @@
|
||||
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 person with veil
|
||||
1F470 1F3FB ; fully-qualified # 👰🏻 E1.0 person with veil: light skin tone
|
||||
1F470 1F3FC ; fully-qualified # 👰🏼 E1.0 person with veil: medium-light skin tone
|
||||
1F470 1F3FD ; fully-qualified # 👰🏽 E1.0 person with veil: medium skin tone
|
||||
1F470 1F3FE ; fully-qualified # 👰🏾 E1.0 person with veil: medium-dark skin tone
|
||||
1F470 1F3FF ; fully-qualified # 👰🏿 E1.0 person with veil: 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
|
||||
@ -2014,24 +1976,24 @@
|
||||
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 white cane
|
||||
1F9D1 1F3FB 200D 1F9AF ; fully-qualified # 🧑🏻🦯 E12.1 person with white cane: light skin tone
|
||||
1F9D1 1F3FC 200D 1F9AF ; fully-qualified # 🧑🏼🦯 E12.1 person with white cane: medium-light skin tone
|
||||
1F9D1 1F3FD 200D 1F9AF ; fully-qualified # 🧑🏽🦯 E12.1 person with white cane: medium skin tone
|
||||
1F9D1 1F3FE 200D 1F9AF ; fully-qualified # 🧑🏾🦯 E12.1 person with white cane: medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 1F9AF ; fully-qualified # 🧑🏿🦯 E12.1 person with white cane: dark skin tone
|
||||
1F468 200D 1F9AF ; fully-qualified # 👨🦯 E12.0 man with white cane
|
||||
1F468 1F3FB 200D 1F9AF ; fully-qualified # 👨🏻🦯 E12.0 man with white cane: light skin tone
|
||||
1F468 1F3FC 200D 1F9AF ; fully-qualified # 👨🏼🦯 E12.0 man with white cane: medium-light skin tone
|
||||
1F468 1F3FD 200D 1F9AF ; fully-qualified # 👨🏽🦯 E12.0 man with white cane: medium skin tone
|
||||
1F468 1F3FE 200D 1F9AF ; fully-qualified # 👨🏾🦯 E12.0 man with white cane: medium-dark skin tone
|
||||
1F468 1F3FF 200D 1F9AF ; fully-qualified # 👨🏿🦯 E12.0 man with white cane: dark skin tone
|
||||
1F469 200D 1F9AF ; fully-qualified # 👩🦯 E12.0 woman with white cane
|
||||
1F469 1F3FB 200D 1F9AF ; fully-qualified # 👩🏻🦯 E12.0 woman with white cane: light skin tone
|
||||
1F469 1F3FC 200D 1F9AF ; fully-qualified # 👩🏼🦯 E12.0 woman with white cane: medium-light skin tone
|
||||
1F469 1F3FD 200D 1F9AF ; fully-qualified # 👩🏽🦯 E12.0 woman with white cane: medium skin tone
|
||||
1F469 1F3FE 200D 1F9AF ; fully-qualified # 👩🏾🦯 E12.0 woman with white cane: medium-dark skin tone
|
||||
1F469 1F3FF 200D 1F9AF ; fully-qualified # 👩🏿🦯 E12.0 woman with white cane: 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
|
||||
@ -2110,13 +2072,13 @@
|
||||
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 person in suit levitating
|
||||
1F574 ; unqualified # 🕴 E0.7 person in suit levitating
|
||||
1F574 1F3FB ; fully-qualified # 🕴🏻 E4.0 person in suit levitating: light skin tone
|
||||
1F574 1F3FC ; fully-qualified # 🕴🏼 E4.0 person in suit levitating: medium-light skin tone
|
||||
1F574 1F3FD ; fully-qualified # 🕴🏽 E4.0 person in suit levitating: medium skin tone
|
||||
1F574 1F3FE ; fully-qualified # 🕴🏾 E4.0 person in suit levitating: medium-dark skin tone
|
||||
1F574 1F3FF ; fully-qualified # 🕴🏿 E4.0 person in suit levitating: 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
|
||||
@ -2182,6 +2144,7 @@
|
||||
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
|
||||
@ -2730,409 +2693,19 @@
|
||||
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
|
||||
1F48F 1F3FB ; fully-qualified # 💏🏻 E13.1 kiss: light skin tone
|
||||
1F48F 1F3FC ; fully-qualified # 💏🏼 E13.1 kiss: medium-light skin tone
|
||||
1F48F 1F3FD ; fully-qualified # 💏🏽 E13.1 kiss: medium skin tone
|
||||
1F48F 1F3FE ; fully-qualified # 💏🏾 E13.1 kiss: medium-dark skin tone
|
||||
1F48F 1F3FF ; fully-qualified # 💏🏿 E13.1 kiss: dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻❤️💋🧑🏼 E13.1 kiss: person, person, light skin tone, medium-light skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏻❤💋🧑🏼 E13.1 kiss: person, person, light skin tone, medium-light skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻❤️💋🧑🏽 E13.1 kiss: person, person, light skin tone, medium skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏻❤💋🧑🏽 E13.1 kiss: person, person, light skin tone, medium skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻❤️💋🧑🏾 E13.1 kiss: person, person, light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏻❤💋🧑🏾 E13.1 kiss: person, person, light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻❤️💋🧑🏿 E13.1 kiss: person, person, light skin tone, dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏻❤💋🧑🏿 E13.1 kiss: person, person, light skin tone, dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼❤️💋🧑🏻 E13.1 kiss: person, person, medium-light skin tone, light skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏼❤💋🧑🏻 E13.1 kiss: person, person, medium-light skin tone, light skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼❤️💋🧑🏽 E13.1 kiss: person, person, medium-light skin tone, medium skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏼❤💋🧑🏽 E13.1 kiss: person, person, medium-light skin tone, medium skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼❤️💋🧑🏾 E13.1 kiss: person, person, medium-light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏼❤💋🧑🏾 E13.1 kiss: person, person, medium-light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼❤️💋🧑🏿 E13.1 kiss: person, person, medium-light skin tone, dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏼❤💋🧑🏿 E13.1 kiss: person, person, medium-light skin tone, dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽❤️💋🧑🏻 E13.1 kiss: person, person, medium skin tone, light skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏽❤💋🧑🏻 E13.1 kiss: person, person, medium skin tone, light skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽❤️💋🧑🏼 E13.1 kiss: person, person, medium skin tone, medium-light skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏽❤💋🧑🏼 E13.1 kiss: person, person, medium skin tone, medium-light skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽❤️💋🧑🏾 E13.1 kiss: person, person, medium skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏽❤💋🧑🏾 E13.1 kiss: person, person, medium skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽❤️💋🧑🏿 E13.1 kiss: person, person, medium skin tone, dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏽❤💋🧑🏿 E13.1 kiss: person, person, medium skin tone, dark skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾❤️💋🧑🏻 E13.1 kiss: person, person, medium-dark skin tone, light skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏾❤💋🧑🏻 E13.1 kiss: person, person, medium-dark skin tone, light skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾❤️💋🧑🏼 E13.1 kiss: person, person, medium-dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏾❤💋🧑🏼 E13.1 kiss: person, person, medium-dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾❤️💋🧑🏽 E13.1 kiss: person, person, medium-dark skin tone, medium skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏾❤💋🧑🏽 E13.1 kiss: person, person, medium-dark skin tone, medium skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾❤️💋🧑🏿 E13.1 kiss: person, person, medium-dark skin tone, dark skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏾❤💋🧑🏿 E13.1 kiss: person, person, medium-dark skin tone, dark skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿❤️💋🧑🏻 E13.1 kiss: person, person, dark skin tone, light skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏿❤💋🧑🏻 E13.1 kiss: person, person, dark skin tone, light skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿❤️💋🧑🏼 E13.1 kiss: person, person, dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏿❤💋🧑🏼 E13.1 kiss: person, person, dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿❤️💋🧑🏽 E13.1 kiss: person, person, dark skin tone, medium skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏿❤💋🧑🏽 E13.1 kiss: person, person, dark skin tone, medium skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿❤️💋🧑🏾 E13.1 kiss: person, person, dark skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏿❤💋🧑🏾 E13.1 kiss: person, person, dark skin tone, medium-dark skin tone
|
||||
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
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏻❤️💋👨🏻 E13.1 kiss: woman, man, light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏻❤💋👨🏻 E13.1 kiss: woman, man, light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏻❤️💋👨🏼 E13.1 kiss: woman, man, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏻❤💋👨🏼 E13.1 kiss: woman, man, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏻❤️💋👨🏽 E13.1 kiss: woman, man, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏻❤💋👨🏽 E13.1 kiss: woman, man, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏻❤️💋👨🏾 E13.1 kiss: woman, man, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏻❤💋👨🏾 E13.1 kiss: woman, man, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏻❤️💋👨🏿 E13.1 kiss: woman, man, light skin tone, dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏻❤💋👨🏿 E13.1 kiss: woman, man, light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏼❤️💋👨🏻 E13.1 kiss: woman, man, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏼❤💋👨🏻 E13.1 kiss: woman, man, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏼❤️💋👨🏼 E13.1 kiss: woman, man, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏼❤💋👨🏼 E13.1 kiss: woman, man, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏼❤️💋👨🏽 E13.1 kiss: woman, man, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏼❤💋👨🏽 E13.1 kiss: woman, man, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏼❤️💋👨🏾 E13.1 kiss: woman, man, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏼❤💋👨🏾 E13.1 kiss: woman, man, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏼❤️💋👨🏿 E13.1 kiss: woman, man, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏼❤💋👨🏿 E13.1 kiss: woman, man, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏽❤️💋👨🏻 E13.1 kiss: woman, man, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏽❤💋👨🏻 E13.1 kiss: woman, man, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏽❤️💋👨🏼 E13.1 kiss: woman, man, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏽❤💋👨🏼 E13.1 kiss: woman, man, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏽❤️💋👨🏽 E13.1 kiss: woman, man, medium skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏽❤💋👨🏽 E13.1 kiss: woman, man, medium skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏽❤️💋👨🏾 E13.1 kiss: woman, man, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏽❤💋👨🏾 E13.1 kiss: woman, man, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏽❤️💋👨🏿 E13.1 kiss: woman, man, medium skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏽❤💋👨🏿 E13.1 kiss: woman, man, medium skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏾❤️💋👨🏻 E13.1 kiss: woman, man, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏾❤💋👨🏻 E13.1 kiss: woman, man, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏾❤️💋👨🏼 E13.1 kiss: woman, man, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏾❤💋👨🏼 E13.1 kiss: woman, man, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏾❤️💋👨🏽 E13.1 kiss: woman, man, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏾❤💋👨🏽 E13.1 kiss: woman, man, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏾❤️💋👨🏾 E13.1 kiss: woman, man, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏾❤💋👨🏾 E13.1 kiss: woman, man, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏾❤️💋👨🏿 E13.1 kiss: woman, man, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏾❤💋👨🏿 E13.1 kiss: woman, man, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏿❤️💋👨🏻 E13.1 kiss: woman, man, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏿❤💋👨🏻 E13.1 kiss: woman, man, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏿❤️💋👨🏼 E13.1 kiss: woman, man, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏿❤💋👨🏼 E13.1 kiss: woman, man, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏿❤️💋👨🏽 E13.1 kiss: woman, man, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏿❤💋👨🏽 E13.1 kiss: woman, man, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏿❤️💋👨🏾 E13.1 kiss: woman, man, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏿❤💋👨🏾 E13.1 kiss: woman, man, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏿❤️💋👨🏿 E13.1 kiss: woman, man, dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏿❤💋👨🏿 E13.1 kiss: woman, man, dark skin tone
|
||||
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
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏻❤️💋👨🏻 E13.1 kiss: man, man, light skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏻❤💋👨🏻 E13.1 kiss: man, man, light skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏻❤️💋👨🏼 E13.1 kiss: man, man, light skin tone, medium-light skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏻❤💋👨🏼 E13.1 kiss: man, man, light skin tone, medium-light skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏻❤️💋👨🏽 E13.1 kiss: man, man, light skin tone, medium skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏻❤💋👨🏽 E13.1 kiss: man, man, light skin tone, medium skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏻❤️💋👨🏾 E13.1 kiss: man, man, light skin tone, medium-dark skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏻❤💋👨🏾 E13.1 kiss: man, man, light skin tone, medium-dark skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏻❤️💋👨🏿 E13.1 kiss: man, man, light skin tone, dark skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏻❤💋👨🏿 E13.1 kiss: man, man, light skin tone, dark skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏼❤️💋👨🏻 E13.1 kiss: man, man, medium-light skin tone, light skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏼❤💋👨🏻 E13.1 kiss: man, man, medium-light skin tone, light skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏼❤️💋👨🏼 E13.1 kiss: man, man, medium-light skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏼❤💋👨🏼 E13.1 kiss: man, man, medium-light skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏼❤️💋👨🏽 E13.1 kiss: man, man, medium-light skin tone, medium skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏼❤💋👨🏽 E13.1 kiss: man, man, medium-light skin tone, medium skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏼❤️💋👨🏾 E13.1 kiss: man, man, medium-light skin tone, medium-dark skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏼❤💋👨🏾 E13.1 kiss: man, man, medium-light skin tone, medium-dark skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏼❤️💋👨🏿 E13.1 kiss: man, man, medium-light skin tone, dark skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏼❤💋👨🏿 E13.1 kiss: man, man, medium-light skin tone, dark skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏽❤️💋👨🏻 E13.1 kiss: man, man, medium skin tone, light skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏽❤💋👨🏻 E13.1 kiss: man, man, medium skin tone, light skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏽❤️💋👨🏼 E13.1 kiss: man, man, medium skin tone, medium-light skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏽❤💋👨🏼 E13.1 kiss: man, man, medium skin tone, medium-light skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏽❤️💋👨🏽 E13.1 kiss: man, man, medium skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏽❤💋👨🏽 E13.1 kiss: man, man, medium skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏽❤️💋👨🏾 E13.1 kiss: man, man, medium skin tone, medium-dark skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏽❤💋👨🏾 E13.1 kiss: man, man, medium skin tone, medium-dark skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏽❤️💋👨🏿 E13.1 kiss: man, man, medium skin tone, dark skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏽❤💋👨🏿 E13.1 kiss: man, man, medium skin tone, dark skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏾❤️💋👨🏻 E13.1 kiss: man, man, medium-dark skin tone, light skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏾❤💋👨🏻 E13.1 kiss: man, man, medium-dark skin tone, light skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏾❤️💋👨🏼 E13.1 kiss: man, man, medium-dark skin tone, medium-light skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏾❤💋👨🏼 E13.1 kiss: man, man, medium-dark skin tone, medium-light skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏾❤️💋👨🏽 E13.1 kiss: man, man, medium-dark skin tone, medium skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏾❤💋👨🏽 E13.1 kiss: man, man, medium-dark skin tone, medium skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏾❤️💋👨🏾 E13.1 kiss: man, man, medium-dark skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏾❤💋👨🏾 E13.1 kiss: man, man, medium-dark skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏾❤️💋👨🏿 E13.1 kiss: man, man, medium-dark skin tone, dark skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏾❤💋👨🏿 E13.1 kiss: man, man, medium-dark skin tone, dark skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏿❤️💋👨🏻 E13.1 kiss: man, man, dark skin tone, light skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏿❤💋👨🏻 E13.1 kiss: man, man, dark skin tone, light skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏿❤️💋👨🏼 E13.1 kiss: man, man, dark skin tone, medium-light skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏿❤💋👨🏼 E13.1 kiss: man, man, dark skin tone, medium-light skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏿❤️💋👨🏽 E13.1 kiss: man, man, dark skin tone, medium skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏿❤💋👨🏽 E13.1 kiss: man, man, dark skin tone, medium skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏿❤️💋👨🏾 E13.1 kiss: man, man, dark skin tone, medium-dark skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏿❤💋👨🏾 E13.1 kiss: man, man, dark skin tone, medium-dark skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏿❤️💋👨🏿 E13.1 kiss: man, man, dark skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏿❤💋👨🏿 E13.1 kiss: man, man, dark skin tone
|
||||
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
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏻❤️💋👩🏻 E13.1 kiss: woman, woman, light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏻❤💋👩🏻 E13.1 kiss: woman, woman, light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏻❤️💋👩🏼 E13.1 kiss: woman, woman, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏻❤💋👩🏼 E13.1 kiss: woman, woman, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏻❤️💋👩🏽 E13.1 kiss: woman, woman, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏻❤💋👩🏽 E13.1 kiss: woman, woman, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏻❤️💋👩🏾 E13.1 kiss: woman, woman, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏻❤💋👩🏾 E13.1 kiss: woman, woman, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏻❤️💋👩🏿 E13.1 kiss: woman, woman, light skin tone, dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏻❤💋👩🏿 E13.1 kiss: woman, woman, light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏼❤️💋👩🏻 E13.1 kiss: woman, woman, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏼❤💋👩🏻 E13.1 kiss: woman, woman, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏼❤️💋👩🏼 E13.1 kiss: woman, woman, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏼❤💋👩🏼 E13.1 kiss: woman, woman, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏼❤️💋👩🏽 E13.1 kiss: woman, woman, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏼❤💋👩🏽 E13.1 kiss: woman, woman, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏼❤️💋👩🏾 E13.1 kiss: woman, woman, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏼❤💋👩🏾 E13.1 kiss: woman, woman, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏼❤️💋👩🏿 E13.1 kiss: woman, woman, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏼❤💋👩🏿 E13.1 kiss: woman, woman, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏽❤️💋👩🏻 E13.1 kiss: woman, woman, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏽❤💋👩🏻 E13.1 kiss: woman, woman, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏽❤️💋👩🏼 E13.1 kiss: woman, woman, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏽❤💋👩🏼 E13.1 kiss: woman, woman, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏽❤️💋👩🏽 E13.1 kiss: woman, woman, medium skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏽❤💋👩🏽 E13.1 kiss: woman, woman, medium skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏽❤️💋👩🏾 E13.1 kiss: woman, woman, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏽❤💋👩🏾 E13.1 kiss: woman, woman, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏽❤️💋👩🏿 E13.1 kiss: woman, woman, medium skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏽❤💋👩🏿 E13.1 kiss: woman, woman, medium skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏾❤️💋👩🏻 E13.1 kiss: woman, woman, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏾❤💋👩🏻 E13.1 kiss: woman, woman, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏾❤️💋👩🏼 E13.1 kiss: woman, woman, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏾❤💋👩🏼 E13.1 kiss: woman, woman, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏾❤️💋👩🏽 E13.1 kiss: woman, woman, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏾❤💋👩🏽 E13.1 kiss: woman, woman, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏾❤️💋👩🏾 E13.1 kiss: woman, woman, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏾❤💋👩🏾 E13.1 kiss: woman, woman, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏾❤️💋👩🏿 E13.1 kiss: woman, woman, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏾❤💋👩🏿 E13.1 kiss: woman, woman, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏿❤️💋👩🏻 E13.1 kiss: woman, woman, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏿❤💋👩🏻 E13.1 kiss: woman, woman, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏿❤️💋👩🏼 E13.1 kiss: woman, woman, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏿❤💋👩🏼 E13.1 kiss: woman, woman, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏿❤️💋👩🏽 E13.1 kiss: woman, woman, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏿❤💋👩🏽 E13.1 kiss: woman, woman, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏿❤️💋👩🏾 E13.1 kiss: woman, woman, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏿❤💋👩🏾 E13.1 kiss: woman, woman, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏿❤️💋👩🏿 E13.1 kiss: woman, woman, dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏿❤💋👩🏿 E13.1 kiss: woman, woman, dark skin tone
|
||||
1F491 ; fully-qualified # 💑 E0.6 couple with heart
|
||||
1F491 1F3FB ; fully-qualified # 💑🏻 E13.1 couple with heart: light skin tone
|
||||
1F491 1F3FC ; fully-qualified # 💑🏼 E13.1 couple with heart: medium-light skin tone
|
||||
1F491 1F3FD ; fully-qualified # 💑🏽 E13.1 couple with heart: medium skin tone
|
||||
1F491 1F3FE ; fully-qualified # 💑🏾 E13.1 couple with heart: medium-dark skin tone
|
||||
1F491 1F3FF ; fully-qualified # 💑🏿 E13.1 couple with heart: dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻❤️🧑🏼 E13.1 couple with heart: person, person, light skin tone, medium-light skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏻❤🧑🏼 E13.1 couple with heart: person, person, light skin tone, medium-light skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻❤️🧑🏽 E13.1 couple with heart: person, person, light skin tone, medium skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏻❤🧑🏽 E13.1 couple with heart: person, person, light skin tone, medium skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻❤️🧑🏾 E13.1 couple with heart: person, person, light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏻❤🧑🏾 E13.1 couple with heart: person, person, light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻❤️🧑🏿 E13.1 couple with heart: person, person, light skin tone, dark skin tone
|
||||
1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏻❤🧑🏿 E13.1 couple with heart: person, person, light skin tone, dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼❤️🧑🏻 E13.1 couple with heart: person, person, medium-light skin tone, light skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏼❤🧑🏻 E13.1 couple with heart: person, person, medium-light skin tone, light skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼❤️🧑🏽 E13.1 couple with heart: person, person, medium-light skin tone, medium skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏼❤🧑🏽 E13.1 couple with heart: person, person, medium-light skin tone, medium skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼❤️🧑🏾 E13.1 couple with heart: person, person, medium-light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏼❤🧑🏾 E13.1 couple with heart: person, person, medium-light skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼❤️🧑🏿 E13.1 couple with heart: person, person, medium-light skin tone, dark skin tone
|
||||
1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏼❤🧑🏿 E13.1 couple with heart: person, person, medium-light skin tone, dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽❤️🧑🏻 E13.1 couple with heart: person, person, medium skin tone, light skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏽❤🧑🏻 E13.1 couple with heart: person, person, medium skin tone, light skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽❤️🧑🏼 E13.1 couple with heart: person, person, medium skin tone, medium-light skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏽❤🧑🏼 E13.1 couple with heart: person, person, medium skin tone, medium-light skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽❤️🧑🏾 E13.1 couple with heart: person, person, medium skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏽❤🧑🏾 E13.1 couple with heart: person, person, medium skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽❤️🧑🏿 E13.1 couple with heart: person, person, medium skin tone, dark skin tone
|
||||
1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏽❤🧑🏿 E13.1 couple with heart: person, person, medium skin tone, dark skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾❤️🧑🏻 E13.1 couple with heart: person, person, medium-dark skin tone, light skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏾❤🧑🏻 E13.1 couple with heart: person, person, medium-dark skin tone, light skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾❤️🧑🏼 E13.1 couple with heart: person, person, medium-dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏾❤🧑🏼 E13.1 couple with heart: person, person, medium-dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾❤️🧑🏽 E13.1 couple with heart: person, person, medium-dark skin tone, medium skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏾❤🧑🏽 E13.1 couple with heart: person, person, medium-dark skin tone, medium skin tone
|
||||
1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾❤️🧑🏿 E13.1 couple with heart: person, person, medium-dark skin tone, dark skin tone
|
||||
1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏾❤🧑🏿 E13.1 couple with heart: person, person, medium-dark skin tone, dark skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿❤️🧑🏻 E13.1 couple with heart: person, person, dark skin tone, light skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏿❤🧑🏻 E13.1 couple with heart: person, person, dark skin tone, light skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿❤️🧑🏼 E13.1 couple with heart: person, person, dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏿❤🧑🏼 E13.1 couple with heart: person, person, dark skin tone, medium-light skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿❤️🧑🏽 E13.1 couple with heart: person, person, dark skin tone, medium skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏿❤🧑🏽 E13.1 couple with heart: person, person, dark skin tone, medium skin tone
|
||||
1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿❤️🧑🏾 E13.1 couple with heart: person, person, dark skin tone, medium-dark skin tone
|
||||
1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏿❤🧑🏾 E13.1 couple with heart: person, person, dark skin tone, medium-dark skin tone
|
||||
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
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏻❤️👨🏻 E13.1 couple with heart: woman, man, light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏻❤👨🏻 E13.1 couple with heart: woman, man, light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏻❤️👨🏼 E13.1 couple with heart: woman, man, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏻❤👨🏼 E13.1 couple with heart: woman, man, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏻❤️👨🏽 E13.1 couple with heart: woman, man, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏻❤👨🏽 E13.1 couple with heart: woman, man, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏻❤️👨🏾 E13.1 couple with heart: woman, man, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏻❤👨🏾 E13.1 couple with heart: woman, man, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏻❤️👨🏿 E13.1 couple with heart: woman, man, light skin tone, dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏻❤👨🏿 E13.1 couple with heart: woman, man, light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏼❤️👨🏻 E13.1 couple with heart: woman, man, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏼❤👨🏻 E13.1 couple with heart: woman, man, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏼❤️👨🏼 E13.1 couple with heart: woman, man, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏼❤👨🏼 E13.1 couple with heart: woman, man, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏼❤️👨🏽 E13.1 couple with heart: woman, man, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏼❤👨🏽 E13.1 couple with heart: woman, man, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏼❤️👨🏾 E13.1 couple with heart: woman, man, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏼❤👨🏾 E13.1 couple with heart: woman, man, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏼❤️👨🏿 E13.1 couple with heart: woman, man, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏼❤👨🏿 E13.1 couple with heart: woman, man, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏽❤️👨🏻 E13.1 couple with heart: woman, man, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏽❤👨🏻 E13.1 couple with heart: woman, man, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏽❤️👨🏼 E13.1 couple with heart: woman, man, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏽❤👨🏼 E13.1 couple with heart: woman, man, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏽❤️👨🏽 E13.1 couple with heart: woman, man, medium skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏽❤👨🏽 E13.1 couple with heart: woman, man, medium skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏽❤️👨🏾 E13.1 couple with heart: woman, man, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏽❤👨🏾 E13.1 couple with heart: woman, man, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏽❤️👨🏿 E13.1 couple with heart: woman, man, medium skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏽❤👨🏿 E13.1 couple with heart: woman, man, medium skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏾❤️👨🏻 E13.1 couple with heart: woman, man, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏾❤👨🏻 E13.1 couple with heart: woman, man, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏾❤️👨🏼 E13.1 couple with heart: woman, man, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏾❤👨🏼 E13.1 couple with heart: woman, man, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏾❤️👨🏽 E13.1 couple with heart: woman, man, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏾❤👨🏽 E13.1 couple with heart: woman, man, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏾❤️👨🏾 E13.1 couple with heart: woman, man, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏾❤👨🏾 E13.1 couple with heart: woman, man, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏾❤️👨🏿 E13.1 couple with heart: woman, man, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏾❤👨🏿 E13.1 couple with heart: woman, man, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏿❤️👨🏻 E13.1 couple with heart: woman, man, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏿❤👨🏻 E13.1 couple with heart: woman, man, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏿❤️👨🏼 E13.1 couple with heart: woman, man, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏿❤👨🏼 E13.1 couple with heart: woman, man, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏿❤️👨🏽 E13.1 couple with heart: woman, man, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏿❤👨🏽 E13.1 couple with heart: woman, man, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏿❤️👨🏾 E13.1 couple with heart: woman, man, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏿❤👨🏾 E13.1 couple with heart: woman, man, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏿❤️👨🏿 E13.1 couple with heart: woman, man, dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏿❤👨🏿 E13.1 couple with heart: woman, man, dark skin tone
|
||||
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
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏻❤️👨🏻 E13.1 couple with heart: man, man, light skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏻❤👨🏻 E13.1 couple with heart: man, man, light skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏻❤️👨🏼 E13.1 couple with heart: man, man, light skin tone, medium-light skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏻❤👨🏼 E13.1 couple with heart: man, man, light skin tone, medium-light skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏻❤️👨🏽 E13.1 couple with heart: man, man, light skin tone, medium skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏻❤👨🏽 E13.1 couple with heart: man, man, light skin tone, medium skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏻❤️👨🏾 E13.1 couple with heart: man, man, light skin tone, medium-dark skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏻❤👨🏾 E13.1 couple with heart: man, man, light skin tone, medium-dark skin tone
|
||||
1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏻❤️👨🏿 E13.1 couple with heart: man, man, light skin tone, dark skin tone
|
||||
1F468 1F3FB 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏻❤👨🏿 E13.1 couple with heart: man, man, light skin tone, dark skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏼❤️👨🏻 E13.1 couple with heart: man, man, medium-light skin tone, light skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏼❤👨🏻 E13.1 couple with heart: man, man, medium-light skin tone, light skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏼❤️👨🏼 E13.1 couple with heart: man, man, medium-light skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏼❤👨🏼 E13.1 couple with heart: man, man, medium-light skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏼❤️👨🏽 E13.1 couple with heart: man, man, medium-light skin tone, medium skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏼❤👨🏽 E13.1 couple with heart: man, man, medium-light skin tone, medium skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏼❤️👨🏾 E13.1 couple with heart: man, man, medium-light skin tone, medium-dark skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏼❤👨🏾 E13.1 couple with heart: man, man, medium-light skin tone, medium-dark skin tone
|
||||
1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏼❤️👨🏿 E13.1 couple with heart: man, man, medium-light skin tone, dark skin tone
|
||||
1F468 1F3FC 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏼❤👨🏿 E13.1 couple with heart: man, man, medium-light skin tone, dark skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏽❤️👨🏻 E13.1 couple with heart: man, man, medium skin tone, light skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏽❤👨🏻 E13.1 couple with heart: man, man, medium skin tone, light skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏽❤️👨🏼 E13.1 couple with heart: man, man, medium skin tone, medium-light skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏽❤👨🏼 E13.1 couple with heart: man, man, medium skin tone, medium-light skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏽❤️👨🏽 E13.1 couple with heart: man, man, medium skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏽❤👨🏽 E13.1 couple with heart: man, man, medium skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏽❤️👨🏾 E13.1 couple with heart: man, man, medium skin tone, medium-dark skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏽❤👨🏾 E13.1 couple with heart: man, man, medium skin tone, medium-dark skin tone
|
||||
1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏽❤️👨🏿 E13.1 couple with heart: man, man, medium skin tone, dark skin tone
|
||||
1F468 1F3FD 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏽❤👨🏿 E13.1 couple with heart: man, man, medium skin tone, dark skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏾❤️👨🏻 E13.1 couple with heart: man, man, medium-dark skin tone, light skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏾❤👨🏻 E13.1 couple with heart: man, man, medium-dark skin tone, light skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏾❤️👨🏼 E13.1 couple with heart: man, man, medium-dark skin tone, medium-light skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏾❤👨🏼 E13.1 couple with heart: man, man, medium-dark skin tone, medium-light skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏾❤️👨🏽 E13.1 couple with heart: man, man, medium-dark skin tone, medium skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏾❤👨🏽 E13.1 couple with heart: man, man, medium-dark skin tone, medium skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏾❤️👨🏾 E13.1 couple with heart: man, man, medium-dark skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏾❤👨🏾 E13.1 couple with heart: man, man, medium-dark skin tone
|
||||
1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏾❤️👨🏿 E13.1 couple with heart: man, man, medium-dark skin tone, dark skin tone
|
||||
1F468 1F3FE 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏾❤👨🏿 E13.1 couple with heart: man, man, medium-dark skin tone, dark skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏿❤️👨🏻 E13.1 couple with heart: man, man, dark skin tone, light skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏿❤👨🏻 E13.1 couple with heart: man, man, dark skin tone, light skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏿❤️👨🏼 E13.1 couple with heart: man, man, dark skin tone, medium-light skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏿❤👨🏼 E13.1 couple with heart: man, man, dark skin tone, medium-light skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏿❤️👨🏽 E13.1 couple with heart: man, man, dark skin tone, medium skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏿❤👨🏽 E13.1 couple with heart: man, man, dark skin tone, medium skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏿❤️👨🏾 E13.1 couple with heart: man, man, dark skin tone, medium-dark skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏿❤👨🏾 E13.1 couple with heart: man, man, dark skin tone, medium-dark skin tone
|
||||
1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏿❤️👨🏿 E13.1 couple with heart: man, man, dark skin tone
|
||||
1F468 1F3FF 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏿❤👨🏿 E13.1 couple with heart: man, man, dark skin tone
|
||||
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
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏻❤️👩🏻 E13.1 couple with heart: woman, woman, light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏻❤👩🏻 E13.1 couple with heart: woman, woman, light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏻❤️👩🏼 E13.1 couple with heart: woman, woman, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏻❤👩🏼 E13.1 couple with heart: woman, woman, light skin tone, medium-light skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏻❤️👩🏽 E13.1 couple with heart: woman, woman, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏻❤👩🏽 E13.1 couple with heart: woman, woman, light skin tone, medium skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏻❤️👩🏾 E13.1 couple with heart: woman, woman, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏻❤👩🏾 E13.1 couple with heart: woman, woman, light skin tone, medium-dark skin tone
|
||||
1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏻❤️👩🏿 E13.1 couple with heart: woman, woman, light skin tone, dark skin tone
|
||||
1F469 1F3FB 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏻❤👩🏿 E13.1 couple with heart: woman, woman, light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏼❤️👩🏻 E13.1 couple with heart: woman, woman, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏼❤👩🏻 E13.1 couple with heart: woman, woman, medium-light skin tone, light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏼❤️👩🏼 E13.1 couple with heart: woman, woman, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏼❤👩🏼 E13.1 couple with heart: woman, woman, medium-light skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏼❤️👩🏽 E13.1 couple with heart: woman, woman, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏼❤👩🏽 E13.1 couple with heart: woman, woman, medium-light skin tone, medium skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏼❤️👩🏾 E13.1 couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏼❤👩🏾 E13.1 couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone
|
||||
1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏼❤️👩🏿 E13.1 couple with heart: woman, woman, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FC 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏼❤👩🏿 E13.1 couple with heart: woman, woman, medium-light skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏽❤️👩🏻 E13.1 couple with heart: woman, woman, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏽❤👩🏻 E13.1 couple with heart: woman, woman, medium skin tone, light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏽❤️👩🏼 E13.1 couple with heart: woman, woman, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏽❤👩🏼 E13.1 couple with heart: woman, woman, medium skin tone, medium-light skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏽❤️👩🏽 E13.1 couple with heart: woman, woman, medium skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏽❤👩🏽 E13.1 couple with heart: woman, woman, medium skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏽❤️👩🏾 E13.1 couple with heart: woman, woman, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏽❤👩🏾 E13.1 couple with heart: woman, woman, medium skin tone, medium-dark skin tone
|
||||
1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏽❤️👩🏿 E13.1 couple with heart: woman, woman, medium skin tone, dark skin tone
|
||||
1F469 1F3FD 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏽❤👩🏿 E13.1 couple with heart: woman, woman, medium skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏾❤️👩🏻 E13.1 couple with heart: woman, woman, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏾❤👩🏻 E13.1 couple with heart: woman, woman, medium-dark skin tone, light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏾❤️👩🏼 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏾❤👩🏼 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏾❤️👩🏽 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏾❤👩🏽 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏾❤️👩🏾 E13.1 couple with heart: woman, woman, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏾❤👩🏾 E13.1 couple with heart: woman, woman, medium-dark skin tone
|
||||
1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏾❤️👩🏿 E13.1 couple with heart: woman, woman, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FE 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏾❤👩🏿 E13.1 couple with heart: woman, woman, medium-dark skin tone, dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏿❤️👩🏻 E13.1 couple with heart: woman, woman, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏿❤👩🏻 E13.1 couple with heart: woman, woman, dark skin tone, light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏿❤️👩🏼 E13.1 couple with heart: woman, woman, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏿❤👩🏼 E13.1 couple with heart: woman, woman, dark skin tone, medium-light skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏿❤️👩🏽 E13.1 couple with heart: woman, woman, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏿❤👩🏽 E13.1 couple with heart: woman, woman, dark skin tone, medium skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏿❤️👩🏾 E13.1 couple with heart: woman, woman, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏿❤👩🏾 E13.1 couple with heart: woman, woman, dark skin tone, medium-dark skin tone
|
||||
1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏿❤️👩🏿 E13.1 couple with heart: woman, woman, dark skin tone
|
||||
1F469 1F3FF 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏿❤👩🏿 E13.1 couple with heart: woman, woman, dark skin tone
|
||||
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
|
||||
@ -3168,8 +2741,8 @@
|
||||
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
|
||||
1F463 ; fully-qualified # 👣 E0.6 footprints
|
||||
|
||||
# People & Body subtotal: 2899
|
||||
# People & Body subtotal: 494 w/o modifiers
|
||||
# People & Body subtotal: 2480
|
||||
# People & Body subtotal: 490 w/o modifiers
|
||||
|
||||
# group: Component
|
||||
|
||||
@ -3867,7 +3440,7 @@
|
||||
1F94C ; fully-qualified # 🥌 E5.0 curling stone
|
||||
|
||||
# subgroup: game
|
||||
1F3AF ; fully-qualified # 🎯 E0.6 bullseye
|
||||
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
|
||||
@ -4080,6 +3653,8 @@
|
||||
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
|
||||
@ -4168,7 +3743,7 @@
|
||||
1F5E1 ; unqualified # 🗡 E0.7 dagger
|
||||
2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords
|
||||
2694 ; unqualified # ⚔ E1.0 crossed swords
|
||||
1F52B ; fully-qualified # 🔫 E0.6 water pistol
|
||||
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
|
||||
@ -4183,7 +3758,7 @@
|
||||
1F5DC ; unqualified # 🗜 E0.7 clamp
|
||||
2696 FE0F ; fully-qualified # ⚖️ E1.0 balance scale
|
||||
2696 ; unqualified # ⚖ E1.0 balance scale
|
||||
1F9AF ; fully-qualified # 🦯 E12.0 white cane
|
||||
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
|
||||
@ -4247,8 +3822,8 @@
|
||||
1F5FF ; fully-qualified # 🗿 E0.6 moai
|
||||
1FAA7 ; fully-qualified # 🪧 E13.0 placard
|
||||
|
||||
# Objects subtotal: 299
|
||||
# Objects subtotal: 299 w/o modifiers
|
||||
# Objects subtotal: 301
|
||||
# Objects subtotal: 301 w/o modifiers
|
||||
|
||||
# group: Symbols
|
||||
|
||||
@ -4403,34 +3978,11 @@
|
||||
26A7 FE0F ; fully-qualified # ⚧️ E13.0 transgender symbol
|
||||
26A7 ; unqualified # ⚧ E13.0 transgender symbol
|
||||
|
||||
# subgroup: math
|
||||
2716 FE0F ; fully-qualified # ✖️ E0.6 multiply
|
||||
2716 ; unqualified # ✖ E0.6 multiply
|
||||
2795 ; fully-qualified # ➕ E0.6 plus
|
||||
2796 ; fully-qualified # ➖ E0.6 minus
|
||||
2797 ; fully-qualified # ➗ E0.6 divide
|
||||
267E FE0F ; fully-qualified # ♾️ E11.0 infinity
|
||||
267E ; unqualified # ♾ E11.0 infinity
|
||||
|
||||
# subgroup: punctuation
|
||||
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 red question mark
|
||||
2754 ; fully-qualified # ❔ E0.6 white question mark
|
||||
2755 ; fully-qualified # ❕ E0.6 white exclamation mark
|
||||
2757 ; fully-qualified # ❗ E0.6 red exclamation mark
|
||||
3030 FE0F ; fully-qualified # 〰️ E0.6 wavy dash
|
||||
3030 ; unqualified # 〰 E0.6 wavy dash
|
||||
|
||||
# subgroup: currency
|
||||
1F4B1 ; fully-qualified # 💱 E0.6 currency exchange
|
||||
1F4B2 ; fully-qualified # 💲 E0.6 heavy dollar sign
|
||||
|
||||
# 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
|
||||
@ -4444,8 +3996,13 @@
|
||||
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
|
||||
@ -4456,6 +4013,16 @@
|
||||
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
|
||||
@ -4581,8 +4148,8 @@
|
||||
1F533 ; fully-qualified # 🔳 E0.6 white square button
|
||||
1F532 ; fully-qualified # 🔲 E0.6 black square button
|
||||
|
||||
# Symbols subtotal: 301
|
||||
# Symbols subtotal: 301 w/o modifiers
|
||||
# Symbols subtotal: 299
|
||||
# Symbols subtotal: 299 w/o modifiers
|
||||
|
||||
# group: Flags
|
||||
|
||||
@ -4871,9 +4438,9 @@
|
||||
# Flags subtotal: 275 w/o modifiers
|
||||
|
||||
# Status Counts
|
||||
# fully-qualified : 3512
|
||||
# minimally-qualified : 817
|
||||
# unqualified : 252
|
||||
# fully-qualified : 3290
|
||||
# minimally-qualified : 614
|
||||
# unqualified : 250
|
||||
# component : 9
|
||||
|
||||
#EOF
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 773 B |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -13,23 +13,13 @@
|
||||
</description>
|
||||
<translation/>
|
||||
<languages>
|
||||
<lang>cs</lang>
|
||||
<lang>de</lang>
|
||||
<lang>el</lang>
|
||||
<lang>en</lang>
|
||||
<lang>eo</lang>
|
||||
<lang>et</lang>
|
||||
<lang>fi</lang>
|
||||
<lang>fr</lang>
|
||||
<lang>hu</lang>
|
||||
<lang>it</lang>
|
||||
<lang>ja</lang>
|
||||
<lang>ml</lang>
|
||||
<lang>nl</lang>
|
||||
<lang>pl</lang>
|
||||
<lang>ro</lang>
|
||||
<lang>ru</lang>
|
||||
<lang>sv</lang>
|
||||
<lang>zh_CN</lang>
|
||||
</languages>
|
||||
<content_rating type="oars-1.0">
|
||||
@ -53,8 +43,6 @@
|
||||
<url type="homepage">https://github.com/Nheko-Reborn/nheko</url>
|
||||
<update_contact>https://github.com/Nheko-Reborn</update_contact>
|
||||
<releases>
|
||||
<release date="2021-04-23" version="0.8.2"/>
|
||||
<release date="2021-01-27" version="0.8.1"/>
|
||||
<release date="2021-01-21" version="0.8.0"/>
|
||||
<release date="2020-06-12" version="0.7.2"/>
|
||||
<release date="2020-04-24" version="0.7.1"/>
|
||||
@ -69,11 +57,12 @@
|
||||
<release date="2018-08-12" version="0.5.3"/>
|
||||
<release date="2018-07-28" version="0.5.2"/>
|
||||
</releases>
|
||||
<developer_name>Nheko Reborn</developer_name>
|
||||
<url type="bugtracker">https://github.com/Nheko-Reborn/nheko/issues</url>
|
||||
<url type="help">https://github.com/Nheko-Reborn/nheko/</url>
|
||||
<url type="translate">https://weblate.nheko.im/projects/nheko/</url>
|
||||
|
||||
<!--FIXME: where to donate to the application-->
|
||||
<url type="donation"/>
|
||||
<developer_name>Nheko Reborn</developer_name>
|
||||
|
||||
<url type="bugtracker">https://github.com/Nheko-Reborn/nheko/issues</url>
|
||||
|
||||
<url type="help">https://github.com/Nheko-Reborn/nheko/</url>
|
||||
|
||||
<url type="translate">https://weblate.nheko.im/projects/nheko/</url>
|
||||
</component>
|
||||
|
@ -2,7 +2,7 @@
|
||||
Name=nheko
|
||||
Version=1.0
|
||||
Comment=Desktop client for Matrix
|
||||
Exec=nheko %u
|
||||
Exec=nheko
|
||||
Icon=nheko
|
||||
Type=Application
|
||||
Categories=Network;InstantMessaging;Qt;
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./ui"
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.6
|
||||
@ -61,7 +57,6 @@ Rectangle {
|
||||
}
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
cached: true
|
||||
|
||||
maskSource: Rectangle {
|
||||
anchors.fill: parent
|
||||
@ -95,9 +90,4 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./ui"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -15,16 +11,8 @@ Popup {
|
||||
property string completerName
|
||||
property var completer
|
||||
property bool bottomToTop: true
|
||||
property bool fullWidth: false
|
||||
property bool centerRowContent: true
|
||||
property int avatarHeight: 24
|
||||
property int avatarWidth: 24
|
||||
property int rowMargin: 0
|
||||
property int rowSpacing: 5
|
||||
property alias count: listView.count
|
||||
|
||||
signal completionClicked(string completion)
|
||||
signal completionSelected(string id)
|
||||
|
||||
function up() {
|
||||
if (bottomToTop)
|
||||
@ -61,18 +49,9 @@ Popup {
|
||||
return null;
|
||||
}
|
||||
|
||||
function finishCompletion() {
|
||||
if (popup.completerName == "room")
|
||||
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid);
|
||||
|
||||
}
|
||||
|
||||
onCompleterNameChanged: {
|
||||
if (completerName) {
|
||||
if (completerName == "user")
|
||||
completer = TimelineManager.completerFor(completerName, TimelineManager.timeline.roomId());
|
||||
else
|
||||
completer = TimelineManager.completerFor(completerName);
|
||||
completer = TimelineManager.timeline.input.completerFor(completerName);
|
||||
completer.setSearchString("");
|
||||
} else {
|
||||
completer = undefined;
|
||||
@ -91,31 +70,22 @@ Popup {
|
||||
id: listView
|
||||
|
||||
anchors.fill: parent
|
||||
implicitWidth: fullWidth ? parent.width : contentItem.childrenRect.width
|
||||
implicitWidth: contentItem.childrenRect.width
|
||||
model: completer
|
||||
verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom
|
||||
spacing: rowSpacing
|
||||
pixelAligned: true
|
||||
|
||||
delegate: Rectangle {
|
||||
property variant modelData: model
|
||||
|
||||
color: model.index == popup.currentIndex ? colors.highlight : colors.base
|
||||
height: chooser.childrenRect.height + 2 * popup.rowMargin
|
||||
implicitWidth: fullWidth ? popup.width : chooser.childrenRect.width + 4
|
||||
height: chooser.childrenRect.height + 4
|
||||
implicitWidth: chooser.childrenRect.width + 4
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onPositionChanged: popup.currentIndex = model.index
|
||||
onClicked: {
|
||||
popup.completionClicked(completer.completionAt(model.index));
|
||||
if (popup.completerName == "room")
|
||||
popup.completionSelected(model.roomid);
|
||||
|
||||
}
|
||||
onEntered: popup.currentIndex = model.index
|
||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
||||
|
||||
Ripple {
|
||||
rippleTarget: mouseArea
|
||||
@ -128,8 +98,7 @@ Popup {
|
||||
id: chooser
|
||||
|
||||
roleValue: popup.completerName
|
||||
anchors.fill: parent
|
||||
anchors.margins: popup.rowMargin
|
||||
anchors.centerIn: parent
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "user"
|
||||
@ -138,11 +107,10 @@ Popup {
|
||||
id: del
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: rowSpacing
|
||||
|
||||
Avatar {
|
||||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
height: 24
|
||||
width: 24
|
||||
displayName: model.displayName
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
||||
@ -169,7 +137,6 @@ Popup {
|
||||
id: del
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: rowSpacing
|
||||
|
||||
Label {
|
||||
text: model.unicode
|
||||
@ -186,67 +153,6 @@ Popup {
|
||||
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "room"
|
||||
|
||||
RowLayout {
|
||||
id: del
|
||||
|
||||
anchors.centerIn: centerRowContent ? parent : undefined
|
||||
spacing: rowSpacing
|
||||
|
||||
Avatar {
|
||||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.roomName
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: {
|
||||
popup.completionClicked(completer.completionAt(model.index));
|
||||
popup.completionSelected(model.roomid);
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: model.roomName
|
||||
font.pixelSize: popup.avatarHeight * 0.5
|
||||
color: model.index == popup.currentIndex ? colors.highlightedText : colors.text
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "roomAliases"
|
||||
|
||||
RowLayout {
|
||||
id: del
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: rowSpacing
|
||||
|
||||
Avatar {
|
||||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.roomName
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
||||
}
|
||||
|
||||
Label {
|
||||
text: model.roomName
|
||||
color: model.index == popup.currentIndex ? colors.highlightedText : colors.text
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "(" + model.roomAlias + ")"
|
||||
color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,50 +1,44 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 2.1
|
||||
import im.nheko 1.0
|
||||
|
||||
Image {
|
||||
id: stateImg
|
||||
Rectangle {
|
||||
id: indicator
|
||||
|
||||
property bool encrypted: false
|
||||
property int trust: Crypto.Unverified
|
||||
|
||||
width: 16
|
||||
height: 16
|
||||
source: {
|
||||
if (encrypted) {
|
||||
switch (trust) {
|
||||
case Crypto.Verified:
|
||||
return "image://colorimage/:/icons/icons/ui/lock.png?green";
|
||||
case Crypto.TOFU:
|
||||
function getEncryptionImage() {
|
||||
if (encrypted)
|
||||
return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText;
|
||||
default:
|
||||
return "image://colorimage/:/icons/icons/ui/lock.png?#dd3d3d";
|
||||
}
|
||||
} else {
|
||||
else
|
||||
return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d";
|
||||
}
|
||||
}
|
||||
ToolTip.visible: ma.hovered
|
||||
ToolTip.text: {
|
||||
if (!encrypted)
|
||||
|
||||
function getEncryptionTooltip() {
|
||||
if (encrypted)
|
||||
return qsTr("Encrypted");
|
||||
else
|
||||
return qsTr("This message is not encrypted!");
|
||||
|
||||
switch (trust) {
|
||||
case Crypto.Verified:
|
||||
return qsTr("Encrypted by a verified device");
|
||||
case Crypto.TOFU:
|
||||
return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
|
||||
default:
|
||||
return qsTr("Encrypted by an unverified device");
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
color: "transparent"
|
||||
width: 16
|
||||
height: 16
|
||||
ToolTip.visible: ma.containsMouse && indicator.visible
|
||||
ToolTip.text: getEncryptionTooltip()
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Image {
|
||||
id: stateImg
|
||||
|
||||
anchors.fill: parent
|
||||
source: getEncryptionImage()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,117 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./delegates/"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import im.nheko 1.0
|
||||
|
||||
Popup {
|
||||
id: forwardMessagePopup
|
||||
|
||||
property var mid
|
||||
|
||||
function setMessageEventId(mid_in) {
|
||||
mid = mid_in;
|
||||
}
|
||||
|
||||
x: Math.round(parent.width / 2 - width / 2)
|
||||
y: Math.round(parent.height / 2 - height / 2)
|
||||
modal: true
|
||||
palette: colors
|
||||
parent: Overlay.overlay
|
||||
width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8)
|
||||
height: implicitHeight + completerPopup.height + padding * 2
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
onOpened: {
|
||||
completerPopup.open();
|
||||
roomTextInput.forceActiveFocus();
|
||||
}
|
||||
onClosed: {
|
||||
completerPopup.close();
|
||||
}
|
||||
|
||||
Column {
|
||||
id: forwardColumn
|
||||
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
id: titleLabel
|
||||
|
||||
text: qsTr("Forward Message")
|
||||
font.bold: true
|
||||
bottomPadding: 10
|
||||
color: colors.text
|
||||
}
|
||||
|
||||
Reply {
|
||||
id: replyPreview
|
||||
|
||||
modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : {
|
||||
}
|
||||
userColor: TimelineManager.userColor(modelData.userId, colors.window)
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
id: roomTextInput
|
||||
|
||||
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
|
||||
color: colors.text
|
||||
onTextEdited: {
|
||||
completerPopup.completer.searchString = text;
|
||||
}
|
||||
Keys.onPressed: {
|
||||
if (event.key == Qt.Key_Up && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
completerPopup.up();
|
||||
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
completerPopup.down();
|
||||
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||
completerPopup.finishCompletion();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Completer {
|
||||
id: completerPopup
|
||||
|
||||
y: titleLabel.height + replyPreview.height + roomTextInput.height + roomTextInput.bottomPadding + forwardColumn.spacing * 3
|
||||
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
|
||||
completerName: "room"
|
||||
fullWidth: true
|
||||
centerRowContent: false
|
||||
avatarHeight: 24
|
||||
avatarWidth: 24
|
||||
bottomToTop: false
|
||||
closePolicy: Popup.NoAutoClose
|
||||
}
|
||||
|
||||
Connections {
|
||||
onCompletionSelected: {
|
||||
TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id);
|
||||
forwardMessagePopup.close();
|
||||
}
|
||||
onCountChanged: {
|
||||
if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
|
||||
completerPopup.currentIndex = 0;
|
||||
|
||||
}
|
||||
target: completerPopup
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: colors.window
|
||||
}
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(colors.window.r, colors.window.g, colors.window.b, 0.7)
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./ui"
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import im.nheko 1.0 // for cursor shape
|
||||
|
||||
AbstractButton {
|
||||
id: button
|
||||
@ -28,10 +23,11 @@ AbstractButton {
|
||||
source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : ""
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
onPressed: mouse.accepted = false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 2.3
|
||||
import im.nheko 1.0
|
||||
@ -12,15 +8,29 @@ TextEdit {
|
||||
focus: false
|
||||
wrapMode: Text.Wrap
|
||||
selectByMouse: !Settings.mobileMode
|
||||
enabled: selectByMouse
|
||||
color: colors.text
|
||||
onLinkActivated: TimelineManager.openLink(link)
|
||||
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 {
|
||||
TimelineManager.openLink(link);
|
||||
}
|
||||
}
|
||||
ToolTip.visible: hoveredLink
|
||||
ToolTip.text: hoveredLink
|
||||
|
||||
CursorShape {
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
TextField {
|
||||
id: input
|
||||
|
||||
palette: colors
|
||||
|
||||
Rectangle {
|
||||
id: blueBar
|
||||
|
||||
anchors.top: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: colors.highlight
|
||||
height: 1
|
||||
width: parent.width
|
||||
|
||||
Rectangle {
|
||||
id: blackBar
|
||||
|
||||
anchors.verticalCenter: blueBar.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: parent.height + 1
|
||||
width: 0
|
||||
color: colors.text
|
||||
|
||||
states: State {
|
||||
name: "focused"
|
||||
when: input.activeFocus == true
|
||||
|
||||
PropertyChanges {
|
||||
target: blackBar
|
||||
width: blueBar.width
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
from: ""
|
||||
to: "focused"
|
||||
reversible: true
|
||||
|
||||
NumberAnimation {
|
||||
target: blackBar
|
||||
properties: "width"
|
||||
duration: 500
|
||||
easing.type: Easing.InOutQuad
|
||||
alwaysRunToEnd: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: colors.base
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./voip"
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import im.nheko 1.0
|
||||
|
||||
Rectangle {
|
||||
id: inputBar
|
||||
|
||||
color: colors.window
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: row.implicitHeight
|
||||
Layout.preferredHeight: textInput.height + 16
|
||||
Layout.minimumHeight: 40
|
||||
|
||||
Component {
|
||||
@ -26,10 +20,11 @@ Rectangle {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
id: inputBar
|
||||
|
||||
visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) || messageContextMenu.isSender
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 16
|
||||
|
||||
ImageButton {
|
||||
visible: CallManager.callsSupported
|
||||
@ -41,7 +36,7 @@ Rectangle {
|
||||
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
|
||||
Layout.margins: 8
|
||||
Layout.leftMargin: 8
|
||||
onClicked: {
|
||||
if (TimelineManager.timeline) {
|
||||
if (CallManager.haveCallInvite) {
|
||||
@ -49,6 +44,7 @@ Rectangle {
|
||||
} else if (CallManager.isOnCall) {
|
||||
CallManager.hangUp();
|
||||
} else {
|
||||
CallManager.refreshDevices();
|
||||
var dialog = placeCallDialog.createObject(timelineRoot);
|
||||
dialog.open();
|
||||
}
|
||||
@ -62,7 +58,7 @@ Rectangle {
|
||||
width: 22
|
||||
height: 22
|
||||
image: ":/icons/icons/ui/paper-clip-outline.png"
|
||||
Layout.margins: 8
|
||||
Layout.leftMargin: CallManager.callsSupported ? 0 : 8
|
||||
onClicked: TimelineManager.timeline.input.openFileSelection()
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Send a file")
|
||||
@ -81,53 +77,71 @@ Rectangle {
|
||||
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Flickable {
|
||||
id: textInput
|
||||
|
||||
Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter
|
||||
function ensureVisible(r) {
|
||||
if (contentX >= r.x)
|
||||
contentX = r.x;
|
||||
else if (contentX + width <= r.x + r.width)
|
||||
contentX = r.x + r.width - width;
|
||||
if (contentY >= r.y)
|
||||
contentY = r.y;
|
||||
else if (contentY + height <= r.y + r.height)
|
||||
contentY = r.y + r.height - height;
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.maximumHeight: Window.height / 4
|
||||
Layout.minimumHeight: Settings.fontSize
|
||||
implicitWidth: inputBar.width - 4 * (22 + 16) - 24
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
implicitWidth: textArea.width
|
||||
implicitHeight: textArea.height
|
||||
contentWidth: textArea.width
|
||||
contentHeight: textArea.height
|
||||
|
||||
TextArea {
|
||||
id: messageInput
|
||||
id: textArea
|
||||
|
||||
property int completerTriggeredAt: -1
|
||||
|
||||
function insertCompletion(completion) {
|
||||
messageInput.remove(completerTriggeredAt, cursorPosition);
|
||||
messageInput.insert(cursorPosition, completion);
|
||||
textArea.remove(completerTriggeredAt, cursorPosition);
|
||||
textArea.insert(cursorPosition, completion);
|
||||
}
|
||||
|
||||
function openCompleter(pos, type) {
|
||||
completerTriggeredAt = pos;
|
||||
popup.completerName = type;
|
||||
popup.open();
|
||||
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
|
||||
}
|
||||
|
||||
function positionCursorAtEnd() {
|
||||
cursorPosition = messageInput.length;
|
||||
}
|
||||
|
||||
function positionCursorAtStart() {
|
||||
cursorPosition = 0;
|
||||
popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition));
|
||||
}
|
||||
|
||||
text: "asfkajsdf"
|
||||
selectByMouse: true
|
||||
placeholderText: qsTr("Write a message...")
|
||||
placeholderTextColor: colors.buttonText
|
||||
//placeholderTextColor: colors.buttonText
|
||||
// only set the anchors on Qt 5.12 or higher
|
||||
// see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop
|
||||
Component.onCompleted: {
|
||||
if (placeholderTextColor !== undefined)
|
||||
placeholderTextColor = colors.buttonText;
|
||||
|
||||
}
|
||||
color: colors.text
|
||||
width: textInput.width
|
||||
wrapMode: TextEdit.Wrap
|
||||
padding: 8
|
||||
padding: 0
|
||||
focus: true
|
||||
onTextChanged: {
|
||||
if (TimelineManager.timeline)
|
||||
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
|
||||
|
||||
forceActiveFocus();
|
||||
}
|
||||
onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle)
|
||||
onCursorPositionChanged: {
|
||||
if (!TimelineManager.timeline)
|
||||
return ;
|
||||
@ -138,7 +152,7 @@ Rectangle {
|
||||
popup.close();
|
||||
}
|
||||
if (popup.opened)
|
||||
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
|
||||
popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition));
|
||||
|
||||
}
|
||||
onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
||||
@ -149,28 +163,17 @@ Rectangle {
|
||||
if (event.matches(StandardKey.Paste)) {
|
||||
TimelineManager.timeline.input.paste(false);
|
||||
event.accepted = true;
|
||||
} else if (event.key == Qt.Key_Space) {
|
||||
// close popup if user enters space after colon
|
||||
if (cursorPosition == completerTriggeredAt + 1)
|
||||
popup.close();
|
||||
|
||||
if (popup.opened && popup.count <= 0)
|
||||
popup.close();
|
||||
|
||||
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
|
||||
messageInput.clear();
|
||||
textArea.clear();
|
||||
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
|
||||
messageInput.text = TimelineManager.timeline.input.previousText();
|
||||
textArea.text = TimelineManager.timeline.input.previousText();
|
||||
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
|
||||
messageInput.text = TimelineManager.timeline.input.nextText();
|
||||
textArea.text = TimelineManager.timeline.input.nextText();
|
||||
} else if (event.key == Qt.Key_At) {
|
||||
messageInput.openCompleter(cursorPosition, "user");
|
||||
textArea.openCompleter(cursorPosition, "user");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_Colon) {
|
||||
messageInput.openCompleter(cursorPosition, "emoji");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_NumberSign) {
|
||||
messageInput.openCompleter(cursorPosition, "roomAliases");
|
||||
textArea.openCompleter(cursorPosition, "emoji");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_Escape && popup.opened) {
|
||||
completerTriggeredAt = -1;
|
||||
@ -183,12 +186,13 @@ Rectangle {
|
||||
popup.completerName = "";
|
||||
popup.close();
|
||||
if (currentCompletion) {
|
||||
messageInput.insertCompletion(currentCompletion);
|
||||
textArea.insertCompletion(currentCompletion);
|
||||
event.accepted = true;
|
||||
return ;
|
||||
}
|
||||
}
|
||||
TimelineManager.timeline.input.send();
|
||||
textArea.clear();
|
||||
event.accepted = true;
|
||||
} else if (event.key == Qt.Key_Tab) {
|
||||
event.accepted = true;
|
||||
@ -197,22 +201,19 @@ Rectangle {
|
||||
} else {
|
||||
var pos = cursorPosition - 1;
|
||||
while (pos > -1) {
|
||||
var t = messageInput.getText(pos, pos + 1);
|
||||
var t = textArea.getText(pos, pos + 1);
|
||||
console.log('"' + t + '"');
|
||||
if (t == '@') {
|
||||
messageInput.openCompleter(pos, "user");
|
||||
return ;
|
||||
} else if (t == ' ' || t == '\t') {
|
||||
messageInput.openCompleter(pos + 1, "user");
|
||||
if (t == '@' || t == ' ' || t == '\t') {
|
||||
textArea.openCompleter(pos, "user");
|
||||
return ;
|
||||
} else if (t == ':') {
|
||||
messageInput.openCompleter(pos, "emoji");
|
||||
textArea.openCompleter(pos, "emoji");
|
||||
return ;
|
||||
}
|
||||
pos = pos - 1;
|
||||
}
|
||||
// At start of input
|
||||
messageInput.openCompleter(0, "user");
|
||||
textArea.openCompleter(0, "user");
|
||||
}
|
||||
} else if (event.key == Qt.Key_Up && popup.opened) {
|
||||
event.accepted = true;
|
||||
@ -220,93 +221,38 @@ Rectangle {
|
||||
} else if (event.key == Qt.Key_Down && popup.opened) {
|
||||
event.accepted = true;
|
||||
popup.down();
|
||||
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
|
||||
if (cursorPosition == 0) {
|
||||
event.accepted = true;
|
||||
var idx = TimelineManager.timeline.edit ? TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) + 1 : 0;
|
||||
while (true) {
|
||||
var id = TimelineManager.timeline.indexToId(idx);
|
||||
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) {
|
||||
TimelineManager.timeline.edit = id;
|
||||
cursorPosition = 0;
|
||||
Qt.callLater(positionCursorAtEnd);
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
} else if (positionAt(0, cursorRectangle.y) === 0) {
|
||||
event.accepted = true;
|
||||
positionCursorAtStart();
|
||||
}
|
||||
} else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) {
|
||||
if (cursorPosition == messageInput.length && TimelineManager.timeline.edit) {
|
||||
event.accepted = true;
|
||||
var idx = TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) - 1;
|
||||
while (true) {
|
||||
var id = TimelineManager.timeline.indexToId(idx);
|
||||
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) {
|
||||
TimelineManager.timeline.edit = id;
|
||||
Qt.callLater(positionCursorAtStart);
|
||||
break;
|
||||
}
|
||||
idx--;
|
||||
}
|
||||
} else if (positionAt(width, cursorRectangle.y + 2) === messageInput.length) {
|
||||
event.accepted = true;
|
||||
positionCursorAtEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
background: null
|
||||
|
||||
Connections {
|
||||
onActiveTimelineChanged: {
|
||||
messageInput.clear();
|
||||
messageInput.append(TimelineManager.timeline.input.text());
|
||||
messageInput.completerTriggeredAt = -1;
|
||||
onTimelineChanged: {
|
||||
textArea.clear();
|
||||
textArea.append(TimelineManager.timeline.input.text());
|
||||
textArea.completerTriggeredAt = -1;
|
||||
popup.completerName = "";
|
||||
messageInput.forceActiveFocus();
|
||||
}
|
||||
target: TimelineManager
|
||||
}
|
||||
|
||||
Connections {
|
||||
onCompletionClicked: messageInput.insertCompletion(completion)
|
||||
onCompletionClicked: textArea.insertCompletion(completion)
|
||||
target: popup
|
||||
}
|
||||
|
||||
Completer {
|
||||
id: popup
|
||||
|
||||
x: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).x : 0
|
||||
y: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height : 0
|
||||
x: textArea.completerTriggeredAt >= 0 ? textArea.positionToRectangle(textArea.completerTriggeredAt).x : 0
|
||||
y: textArea.completerTriggeredAt >= 0 ? textArea.positionToRectangle(textArea.completerTriggeredAt).y - height : 0
|
||||
}
|
||||
|
||||
Connections {
|
||||
ignoreUnknownSignals: true
|
||||
onInsertText: {
|
||||
messageInput.remove(messageInput.selectionStart, messageInput.selectionEnd);
|
||||
messageInput.insert(messageInput.cursorPosition, text);
|
||||
}
|
||||
onTextChanged: {
|
||||
messageInput.text = newText;
|
||||
messageInput.cursorPosition = newText.length;
|
||||
}
|
||||
onInsertText: textArea.insert(textArea.cursorPosition, text)
|
||||
target: TimelineManager.timeline ? TimelineManager.timeline.input : null
|
||||
}
|
||||
|
||||
Connections {
|
||||
ignoreUnknownSignals: true
|
||||
onReplyChanged: messageInput.forceActiveFocus()
|
||||
onEditChanged: messageInput.forceActiveFocus()
|
||||
target: TimelineManager.timeline
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TimelineManager
|
||||
onFocusInput: messageInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// workaround for wrong cursor shape on some platforms
|
||||
anchors.fill: parent
|
||||
@ -315,6 +261,14 @@ Rectangle {
|
||||
onClicked: TimelineManager.timeline.input.paste(true)
|
||||
}
|
||||
|
||||
NhekoDropArea {
|
||||
anchors.fill: parent
|
||||
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
}
|
||||
|
||||
}
|
||||
@ -323,7 +277,6 @@ Rectangle {
|
||||
id: emojiButton
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.margins: 8
|
||||
hoverEnabled: true
|
||||
width: 22
|
||||
height: 22
|
||||
@ -331,14 +284,12 @@ Rectangle {
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Emoji")
|
||||
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) {
|
||||
messageInput.insert(messageInput.cursorPosition, emoji);
|
||||
TimelineManager.focusMessageInput();
|
||||
textArea.insert(textArea.cursorPosition, emoji);
|
||||
})
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.margins: 8
|
||||
hoverEnabled: true
|
||||
width: 22
|
||||
height: 22
|
||||
@ -348,16 +299,10 @@ Rectangle {
|
||||
ToolTip.text: qsTr("Send")
|
||||
onClicked: {
|
||||
TimelineManager.timeline.input.send();
|
||||
textArea.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: TimelineManager.timeline ? (!TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage)) : false
|
||||
text: qsTr("You don't have permission to send messages in this room")
|
||||
color: colors.text
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,27 +1,19 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./delegates"
|
||||
import "./emoji"
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
import im.nheko 1.0
|
||||
|
||||
ScrollView {
|
||||
clip: false
|
||||
palette: colors
|
||||
padding: 8
|
||||
ScrollBar.horizontal.visible: false
|
||||
|
||||
ListView {
|
||||
ListView {
|
||||
id: chat
|
||||
|
||||
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
|
||||
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2 - 8)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
cacheBuffer: 400
|
||||
model: TimelineManager.timeline
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
pixelAligned: true
|
||||
@ -34,98 +26,6 @@ ScrollView {
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
//closePolicy: Popup.NoAutoClose
|
||||
|
||||
id: messageActions
|
||||
|
||||
property Item attached: null
|
||||
property alias model: row.model
|
||||
// use comma to update on scroll
|
||||
property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null
|
||||
readonly property int padding: 4
|
||||
|
||||
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || messageActionHover.hovered)
|
||||
x: attached ? attachedPos.x : 0
|
||||
y: attached ? attachedPos.y : 0
|
||||
z: 10
|
||||
height: row.implicitHeight + padding * 2
|
||||
width: row.implicitWidth + padding * 2
|
||||
color: colors.window
|
||||
border.color: colors.buttonText
|
||||
border.width: 1
|
||||
radius: padding
|
||||
|
||||
HoverHandler {
|
||||
id: messageActionHover
|
||||
|
||||
grabPermissions: PointerHandler.CanTakeOverFromAnything
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
|
||||
property var model
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: messageActions.padding
|
||||
|
||||
ImageButton {
|
||||
id: editButton
|
||||
|
||||
visible: !!row.model && row.model.isEditable
|
||||
buttonTextColor: colors.buttonText
|
||||
width: 16
|
||||
hoverEnabled: true
|
||||
image: ":/icons/icons/ui/edit.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Edit")
|
||||
onClicked: {
|
||||
if (row.model.isEditable)
|
||||
chat.model.editAction(row.model.id);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
EmojiButton {
|
||||
id: reactButton
|
||||
|
||||
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false
|
||||
width: 16
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("React")
|
||||
emojiPicker: emojiPopup
|
||||
event_id: row.model ? row.model.id : ""
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
id: replyButton
|
||||
|
||||
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.TextMessage) : false
|
||||
width: 16
|
||||
hoverEnabled: true
|
||||
image: ":/icons/icons/ui/mail-reply.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Reply")
|
||||
onClicked: chat.model.replyAction(row.model.id)
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
id: optionsButton
|
||||
|
||||
width: 16
|
||||
hoverEnabled: true
|
||||
image: ":/icons/icons/ui/vertical-ellipsis.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Options")
|
||||
onClicked: messageContextMenu.show(row.model.id, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollHelper {
|
||||
flickable: parent
|
||||
anchors.fill: parent
|
||||
@ -150,12 +50,7 @@ ScrollView {
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: {
|
||||
if (chat.model.reply)
|
||||
chat.model.reply = undefined;
|
||||
else
|
||||
chat.model.edit = undefined;
|
||||
}
|
||||
onActivated: chat.model.reply = undefined
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
@ -167,42 +62,10 @@ ScrollView {
|
||||
sequence: "Alt+Down"
|
||||
onActivated: {
|
||||
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
|
||||
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : null;
|
||||
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+F"
|
||||
onActivated: {
|
||||
if (chat.model.reply) {
|
||||
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
|
||||
forwardMess.setMessageEventId(chat.model.reply);
|
||||
forwardMess.open();
|
||||
chat.model.reply = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+E"
|
||||
onActivated: {
|
||||
chat.model.edit = chat.model.reply;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TimelineManager
|
||||
onFocusChanged: readTimer.running = TimelineManager.isWindowFocused
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: readTimer
|
||||
|
||||
// force current read index to update
|
||||
onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex)
|
||||
interval: 1000
|
||||
}
|
||||
|
||||
Component {
|
||||
id: sectionHeader
|
||||
|
||||
@ -238,29 +101,12 @@ ScrollView {
|
||||
spacing: 8
|
||||
|
||||
Avatar {
|
||||
id: messageUserAvatar
|
||||
|
||||
width: avatarSize
|
||||
height: avatarSize
|
||||
url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
|
||||
displayName: modelData ? modelData.userName : ""
|
||||
userid: modelData ? modelData.userId : ""
|
||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||
ToolTip.visible: avatarHover.hovered
|
||||
ToolTip.text: userid
|
||||
|
||||
HoverHandler {
|
||||
id: avatarHover
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: chat.model
|
||||
onRoomAvatarUrlChanged: {
|
||||
messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "";
|
||||
}
|
||||
onScrollToIndex: chat.positionViewAtIndex(index, ListView.Visible)
|
||||
}
|
||||
|
||||
Label {
|
||||
@ -269,20 +115,13 @@ ScrollView {
|
||||
text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
|
||||
color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window)
|
||||
textFormat: Text.RichText
|
||||
ToolTip.visible: displayNameHover.hovered
|
||||
ToolTip.text: modelData ? modelData.userId : ""
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: chat.model.openUserProfile(modelData.userId)
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: displayNameHover
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
}
|
||||
@ -302,61 +141,17 @@ ScrollView {
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scrollbar
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: wrapper
|
||||
|
||||
property bool scrolledToThis: model.id === chat.model.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
|
||||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
width: chat.delegateMaxWidth
|
||||
height: section ? section.height + timelinerow.height : timelinerow.height
|
||||
|
||||
Rectangle {
|
||||
id: scrollHighlight
|
||||
|
||||
opacity: 0
|
||||
visible: true
|
||||
anchors.fill: timelinerow
|
||||
color: colors.highlight
|
||||
|
||||
states: State {
|
||||
name: "revealed"
|
||||
when: wrapper.scrolledToThis
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
from: ""
|
||||
to: "revealed"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAnimation {
|
||||
target: scrollHighlight
|
||||
properties: "opacity"
|
||||
easing.type: Easing.InOutQuad
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 500
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
target: scrollHighlight
|
||||
properties: "opacity"
|
||||
easing.type: Easing.InOutQuad
|
||||
from: 1
|
||||
to: 0
|
||||
duration: 500
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: chat.model.eventShown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: section
|
||||
|
||||
@ -372,24 +167,7 @@ ScrollView {
|
||||
TimelineRow {
|
||||
id: timelinerow
|
||||
|
||||
property alias hovered: hoverHandler.hovered
|
||||
|
||||
y: section.visible && section.active ? section.y + section.height : 0
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
|
||||
enabled: !Settings.mobileMode
|
||||
onHoveredChanged: {
|
||||
if (hovered) {
|
||||
if (!messageActionHover.hovered) {
|
||||
messageActions.attached = timelinerow;
|
||||
messageActions.model = model;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
y: section.active && section.visible ? section.y + section.height : 0
|
||||
}
|
||||
|
||||
Connections {
|
||||
@ -411,6 +189,4 @@ ScrollView {
|
||||
z: 3
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
|
@ -1,130 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.12
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
id: privacyScreen
|
||||
|
||||
property var timelineRoot
|
||||
property int screenTimeout
|
||||
|
||||
Connections {
|
||||
target: TimelineManager
|
||||
onFocusChanged: {
|
||||
if (TimelineManager.isWindowFocused) {
|
||||
screenSaverTimer.stop();
|
||||
screenSaver.state = "Invisible";
|
||||
} else {
|
||||
if (timelineRoot.visible)
|
||||
screenSaverTimer.start();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: screenSaverTimer
|
||||
|
||||
interval: screenTimeout * 1000
|
||||
running: true
|
||||
onTriggered: {
|
||||
screenSaver.state = "Visible";
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: screenSaver
|
||||
|
||||
state: "Invisible"
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
states: [
|
||||
State {
|
||||
name: "Visible"
|
||||
|
||||
PropertyChanges {
|
||||
target: screenSaver
|
||||
visible: true
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: screenSaver
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "Invisible"
|
||||
|
||||
PropertyChanges {
|
||||
target: screenSaver
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: screenSaver
|
||||
visible: false
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "Visible"
|
||||
to: "Invisible"
|
||||
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
target: screenSaver
|
||||
property: "opacity"
|
||||
duration: 250
|
||||
easing.type: Easing.InQuad
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: screenSaver
|
||||
property: "visible"
|
||||
duration: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
Transition {
|
||||
from: "Invisible"
|
||||
to: "Visible"
|
||||
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
target: screenSaver
|
||||
property: "visible"
|
||||
duration: 0
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: screenSaver
|
||||
property: "opacity"
|
||||
duration: 500
|
||||
easing.type: Easing.InQuad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
FastBlur {
|
||||
id: blur
|
||||
|
||||
anchors.fill: parent
|
||||
source: timelineRoot
|
||||
radius: 50
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import im.nheko 1.0
|
||||
|
||||
Popup {
|
||||
id: quickSwitcher
|
||||
|
||||
property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4)
|
||||
property int textMargin: Math.round(textHeight / 8)
|
||||
|
||||
background: null
|
||||
width: Math.round(parent.width / 2)
|
||||
x: Math.round(parent.width / 2 - width / 2)
|
||||
y: Math.round(parent.height / 4 - height / 2)
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
parent: Overlay.overlay
|
||||
palette: colors
|
||||
onOpened: {
|
||||
completerPopup.open();
|
||||
roomTextInput.forceActiveFocus();
|
||||
}
|
||||
onClosed: {
|
||||
completerPopup.close();
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
id: roomTextInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
|
||||
padding: textMargin
|
||||
color: colors.text
|
||||
onTextEdited: {
|
||||
completerPopup.completer.searchString = text;
|
||||
}
|
||||
Keys.onPressed: {
|
||||
if (event.key == Qt.Key_Up && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
completerPopup.up();
|
||||
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
completerPopup.down();
|
||||
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||
completerPopup.finishCompletion();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Completer {
|
||||
id: completerPopup
|
||||
|
||||
x: roomTextInput.x
|
||||
y: roomTextInput.y + quickSwitcher.textHeight + quickSwitcher.textMargin
|
||||
visible: roomTextInput.length > 0
|
||||
width: parent.width
|
||||
completerName: "room"
|
||||
bottomToTop: false
|
||||
fullWidth: true
|
||||
avatarHeight: quickSwitcher.textHeight
|
||||
avatarWidth: quickSwitcher.textHeight
|
||||
centerRowContent: false
|
||||
rowMargin: Math.round(quickSwitcher.textMargin / 2)
|
||||
rowSpacing: quickSwitcher.textMargin
|
||||
closePolicy: Popup.NoAutoClose
|
||||
}
|
||||
|
||||
Connections {
|
||||
onCompletionSelected: {
|
||||
TimelineManager.setHistoryView(id);
|
||||
TimelineManager.highlightRoom(id);
|
||||
quickSwitcher.close();
|
||||
}
|
||||
onCountChanged: {
|
||||
if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
|
||||
completerPopup.currentIndex = 0;
|
||||
|
||||
}
|
||||
target: completerPopup
|
||||
}
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#aa1E1E1E"
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
import im.nheko 1.0
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./delegates/"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -14,16 +10,15 @@ Rectangle {
|
||||
property var room: TimelineManager.timeline
|
||||
|
||||
Layout.fillWidth: true
|
||||
visible: room && (room.reply || room.edit)
|
||||
visible: room && room.reply
|
||||
// Height of child, plus margins, plus border
|
||||
implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10
|
||||
implicitHeight: replyPreview.height + 10
|
||||
color: colors.window
|
||||
z: 3
|
||||
|
||||
Reply {
|
||||
id: replyPreview
|
||||
|
||||
visible: room && room.reply
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2 * 22 + 3 * 16
|
||||
anchors.right: closeReplyButton.left
|
||||
@ -37,9 +32,8 @@ Rectangle {
|
||||
ImageButton {
|
||||
id: closeReplyButton
|
||||
|
||||
visible: room && room.reply
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16
|
||||
anchors.rightMargin: 15
|
||||
anchors.top: replyPreview.top
|
||||
hoverEnabled: true
|
||||
width: 16
|
||||
@ -50,17 +44,4 @@ Rectangle {
|
||||
onClicked: room.reply = undefined
|
||||
}
|
||||
|
||||
Button {
|
||||
id: closeEditButton
|
||||
|
||||
visible: room && room.edit
|
||||
anchors.left: parent.left
|
||||
anchors.rightMargin: 16
|
||||
anchors.topMargin: 10
|
||||
anchors.top: parent.top
|
||||
//height: 16
|
||||
text: qsTr("Cancel edit")
|
||||
onClicked: room.edit = undefined
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,287 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.3
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: roomSettingsDialog
|
||||
|
||||
property var roomSettings
|
||||
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
minimumWidth: 420
|
||||
minimumHeight: 650
|
||||
palette: colors
|
||||
color: colors.window
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Dialog
|
||||
title: qsTr("Room Settings")
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: roomSettingsDialog.close()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout1
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Avatar {
|
||||
url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: roomSettings.roomName
|
||||
height: 130
|
||||
width: 130
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
if (roomSettings.canChangeAvatar)
|
||||
roomSettings.updateAvatar();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
running: roomSettings.isLoading
|
||||
visible: roomSettings.isLoading
|
||||
}
|
||||
|
||||
Text {
|
||||
id: errorText
|
||||
|
||||
color: "red"
|
||||
visible: opacity > 0
|
||||
opacity: 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: hideErrorAnimation
|
||||
|
||||
running: false
|
||||
|
||||
PauseAnimation {
|
||||
duration: 4000
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: errorText
|
||||
property: 'opacity'
|
||||
to: 0
|
||||
duration: 1000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: roomSettings
|
||||
onDisplayError: {
|
||||
errorText.text = errorMessage;
|
||||
errorText.opacity = 1;
|
||||
hideErrorAnimation.restart();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
MatrixText {
|
||||
text: roomSettings.roomName
|
||||
font.pixelSize: 24
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("%1 member(s)").arg(roomSettings.memberCount)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
image: ":/icons/icons/ui/edit.png"
|
||||
visible: roomSettings.canChangeNameAndTopic
|
||||
onClicked: roomSettings.openEditModal()
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.maximumHeight: 75
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
width: parent.width
|
||||
|
||||
TextArea {
|
||||
text: TimelineManager.escapeEmoji(roomSettings.roomTopic)
|
||||
wrapMode: TextEdit.WordWrap
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true
|
||||
background: null
|
||||
selectByMouse: true
|
||||
color: colors.text
|
||||
horizontalAlignment: TextEdit.AlignHCenter
|
||||
onLinkActivated: TimelineManager.openLink(link)
|
||||
|
||||
CursorShape {
|
||||
anchors.fill: parent
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: 10
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("SETTINGS")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Notifications")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
model: [qsTr("Muted"), qsTr("Mentions only"), qsTr("All messages")]
|
||||
currentIndex: roomSettings.notifications
|
||||
onActivated: {
|
||||
roomSettings.changeNotifications(index);
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: "Room access"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
enabled: roomSettings.canChangeJoinRules
|
||||
model: [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")]
|
||||
currentIndex: roomSettings.accessJoinRules
|
||||
onActivated: {
|
||||
roomSettings.changeAccessRules(index);
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Encryption")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
id: encryptionToggle
|
||||
|
||||
checked: roomSettings.isEncryptionEnabled
|
||||
onClicked: {
|
||||
if (roomSettings.isEncryptionEnabled) {
|
||||
checked = true;
|
||||
return ;
|
||||
}
|
||||
confirmEncryptionDialog.open();
|
||||
}
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
Platform.MessageDialog {
|
||||
id: confirmEncryptionDialog
|
||||
|
||||
title: qsTr("End-to-End Encryption")
|
||||
text: qsTr("Encryption is currently experimental and things might break unexpectedly. <br>
|
||||
Please take note that it can't be disabled afterwards.")
|
||||
modality: Qt.WindowModal
|
||||
onAccepted: {
|
||||
if (roomSettings.isEncryptionEnabled)
|
||||
return ;
|
||||
|
||||
roomSettings.enableEncryption();
|
||||
}
|
||||
onRejected: {
|
||||
encryptionToggle.checked = false;
|
||||
}
|
||||
buttons: Dialog.Ok | Dialog.Cancel
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
visible: roomSettings.isEncryptionEnabled
|
||||
text: qsTr("Respond to key requests")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
visible: roomSettings.isEncryptionEnabled
|
||||
ToolTip.text: qsTr("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.")
|
||||
checked: roomSettings.respondsToKeyRequests
|
||||
onClicked: {
|
||||
roomSettings.changeKeyRequestsPreference(checked);
|
||||
}
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
Item {
|
||||
// for adding extra space between sections
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item {
|
||||
// for adding extra space between sections
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("INFO")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Internal ID")
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: roomSettings.roomId
|
||||
font.pixelSize: 14
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Room Version")
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: roomSettings.roomVersion
|
||||
font.pixelSize: 14
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: qsTr("OK")
|
||||
onClicked: close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,21 @@
|
||||
// Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net>
|
||||
// Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com>
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net>
|
||||
* Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com>
|
||||
*
|
||||
* 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 2 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, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
/*
|
||||
* Shamelessly stolen from:
|
||||
* https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml
|
||||
@ -78,7 +90,7 @@ MouseArea {
|
||||
// Show the scrollbars
|
||||
flickable.flick(0, 0);
|
||||
flickable.contentY = newPos;
|
||||
cancelFlickStateTimer.restart();
|
||||
cancelFlickStateTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 2.1
|
||||
import im.nheko 1.0
|
||||
|
@ -1,10 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./delegates"
|
||||
import "./emoji"
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
@ -16,32 +12,32 @@ Item {
|
||||
height: row.height
|
||||
|
||||
Rectangle {
|
||||
color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent"
|
||||
color: (Settings.messageHoverHighlight && hoverHandler.containsMouse) ? colors.alternateBase : "transparent"
|
||||
anchors.fill: row
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
MouseArea {
|
||||
id: hoverHandler
|
||||
|
||||
acceptedDevices: PointerDevice.GenericPointer
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
preventStealing: false
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.RightButton)
|
||||
messageContextMenu.show(model.id, model.type, model.isEncrypted, row);
|
||||
else
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onSingleTapped: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
onPressAndHold: {
|
||||
messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y));
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onLongPressed: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
|
||||
onDoubleTapped: chat.model.reply = model.id
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.rightMargin: 1
|
||||
anchors.leftMargin: avatarSize + 16
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@ -50,8 +46,6 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: 4
|
||||
Layout.topMargin: 1
|
||||
Layout.bottomMargin: 1
|
||||
|
||||
// fancy reply, if this is a reply
|
||||
Reply {
|
||||
@ -86,41 +80,67 @@ Item {
|
||||
EncryptionIndicator {
|
||||
visible: model.isRoomEncrypted
|
||||
encrypted: model.isEncrypted
|
||||
trust: model.trustlevel
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: model.isEdited || model.id == chat.model.edit
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
height: 16
|
||||
width: 16
|
||||
sourceSize.width: 16
|
||||
sourceSize.height: 16
|
||||
source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? colors.highlight : colors.buttonText)
|
||||
ToolTip.visible: editHovered.hovered
|
||||
ToolTip.text: qsTr("Edited")
|
||||
|
||||
HoverHandler {
|
||||
id: editHovered
|
||||
}
|
||||
|
||||
EmojiButton {
|
||||
id: reactButton
|
||||
|
||||
visible: Settings.buttonsInTimeline
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
width: 16
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("React")
|
||||
emojiPicker: emojiPopup
|
||||
event_id: model.id
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
id: replyButton
|
||||
|
||||
visible: Settings.buttonsInTimeline
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
width: 16
|
||||
hoverEnabled: true
|
||||
image: ":/icons/icons/ui/mail-reply.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Reply")
|
||||
onClicked: chat.model.replyAction(model.id)
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
id: optionsButton
|
||||
|
||||
visible: Settings.buttonsInTimeline
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 16
|
||||
width: 16
|
||||
hoverEnabled: true
|
||||
image: ":/icons/icons/ui/vertical-ellipsis.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Options")
|
||||
onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
text: model.timestamp.toLocaleTimeString(Locale.ShortFormat)
|
||||
text: model.timestamp.toLocaleTimeString("HH:mm")
|
||||
width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
|
||||
color: inactiveColors.text
|
||||
ToolTip.visible: ma.hovered
|
||||
ToolTip.visible: ma.containsMouse
|
||||
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
|
||||
|
||||
HoverHandler {
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./delegates"
|
||||
import "./device-verification"
|
||||
import "./emoji"
|
||||
import "./voip"
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -21,7 +16,7 @@ Page {
|
||||
property var colors: currentActivePalette
|
||||
property var systemInactive
|
||||
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
|
||||
readonly property int avatarSize: 40
|
||||
property int avatarSize: 40
|
||||
property real highlightHue: colors.highlight.hslHue
|
||||
property real highlightSat: colors.highlight.hslSaturation
|
||||
property real highlightLight: colors.highlight.hslLightness
|
||||
@ -35,8 +30,18 @@ Page {
|
||||
EmojiPicker {
|
||||
id: emojiPopup
|
||||
|
||||
width: 7 * 52 + 20
|
||||
height: 6 * 52
|
||||
colors: palette
|
||||
model: TimelineManager.completerFor("allemoji", "")
|
||||
|
||||
model: EmojiProxyModel {
|
||||
category: EmojiCategory.People
|
||||
|
||||
sourceModel: EmojiModel {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
@ -47,14 +52,6 @@ Page {
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomSettingsComponent
|
||||
|
||||
RoomSettings {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mobileCallInviteDialog
|
||||
|
||||
@ -63,158 +60,71 @@ Page {
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: quickSwitcherComponent
|
||||
|
||||
QuickSwitcher {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: forwardCompleterComponent
|
||||
|
||||
ForwardCompleter {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: {
|
||||
var quickSwitch = quickSwitcherComponent.createObject(timelineRoot);
|
||||
TimelineManager.focusTimeline();
|
||||
quickSwitch.open();
|
||||
}
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
Menu {
|
||||
id: messageContextMenu
|
||||
|
||||
property string eventId
|
||||
property string link
|
||||
property string text
|
||||
property int eventType
|
||||
property bool isEncrypted
|
||||
property bool isEditable
|
||||
property bool isSender
|
||||
|
||||
function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
|
||||
function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
|
||||
eventId = eventId_;
|
||||
eventType = eventType_;
|
||||
isEncrypted = isEncrypted_;
|
||||
isEditable = isEditable_;
|
||||
isSender = isSender_;
|
||||
if (text_)
|
||||
text = text_;
|
||||
if (position)
|
||||
popup(position, showAt_);
|
||||
else
|
||||
text = "";
|
||||
if (link_)
|
||||
link = link_;
|
||||
else
|
||||
link = "";
|
||||
if (showAt_)
|
||||
open(showAt_);
|
||||
else
|
||||
open();
|
||||
popup(showAt_);
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: messageContextMenu.text
|
||||
enabled: visible
|
||||
text: qsTr("&Copy")
|
||||
onTriggered: Clipboard.text = messageContextMenu.text
|
||||
}
|
||||
modal: true
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: messageContextMenu.link
|
||||
enabled: visible
|
||||
text: qsTr("Copy &link location")
|
||||
onTriggered: Clipboard.text = messageContextMenu.link
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
id: reactionOption
|
||||
|
||||
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false
|
||||
text: qsTr("Re&act")
|
||||
onTriggered: emojiPopup.show(null, function(emoji) {
|
||||
MenuItem {
|
||||
text: qsTr("React")
|
||||
onClicked: emojiPopup.show(messageContextMenu.parent, function(emoji) {
|
||||
TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji);
|
||||
})
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false
|
||||
text: qsTr("Repl&y")
|
||||
onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
|
||||
MenuItem {
|
||||
text: qsTr("Reply")
|
||||
onClicked: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false)
|
||||
enabled: visible
|
||||
text: qsTr("&Edit")
|
||||
onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Read receip&ts")
|
||||
MenuItem {
|
||||
text: qsTr("Read receipts")
|
||||
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
|
||||
text: qsTr("&Forward")
|
||||
onTriggered: {
|
||||
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
|
||||
forwardMess.setMessageEventId(messageContextMenu.eventId);
|
||||
forwardMess.open();
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Mark as read")
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("&Mark as read")
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
MenuItem {
|
||||
text: qsTr("View raw message")
|
||||
onTriggered: TimelineManager.timeline.viewRawMessage(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
MenuItem {
|
||||
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
|
||||
visible: messageContextMenu.isEncrypted
|
||||
enabled: visible
|
||||
height: visible ? implicitHeight : 0
|
||||
text: qsTr("View decrypted raw message")
|
||||
onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender
|
||||
text: qsTr("Remo&ve message")
|
||||
MenuItem {
|
||||
text: qsTr("Remove message")
|
||||
onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
MenuItem {
|
||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
|
||||
enabled: visible
|
||||
text: qsTr("&Save as")
|
||||
height: visible ? implicitHeight : 0
|
||||
text: qsTr("Save as")
|
||||
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
|
||||
enabled: visible
|
||||
text: qsTr("&Open in external program")
|
||||
onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: messageContextMenu.eventId
|
||||
enabled: visible
|
||||
text: qsTr("Copy link to eve&nt")
|
||||
onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@ -237,6 +147,10 @@ Page {
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TimelineManager.timeline
|
||||
onOpenProfile: {
|
||||
var userProfile = userProfileComponent.createObject(timelineRoot, {
|
||||
"profile": profile
|
||||
@ -245,16 +159,6 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TimelineManager.timeline
|
||||
onOpenRoomSettingsDialog: {
|
||||
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
|
||||
"roomSettings": settings
|
||||
});
|
||||
roomSettings.show();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CallManager
|
||||
onNewInviteState: {
|
||||
@ -283,8 +187,6 @@ Page {
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: timelineLayout
|
||||
|
||||
visible: TimelineManager.timeline != null
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
@ -329,7 +231,7 @@ Page {
|
||||
}
|
||||
|
||||
Loader {
|
||||
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
|
||||
source: CallManager.isOnCall && CallManager.isVideo ? "voip/VideoCall.qml" : ""
|
||||
onLoaded: TimelineManager.setVideoCallItem()
|
||||
}
|
||||
|
||||
@ -369,18 +271,6 @@ Page {
|
||||
|
||||
}
|
||||
|
||||
NhekoDropArea {
|
||||
anchors.fill: parent
|
||||
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PrivacyScreen {
|
||||
anchors.fill: parent
|
||||
visible: Settings.privacyScreen
|
||||
screenTimeout: Settings.privacyScreenTimeout
|
||||
timelineRoot: timelineLayout
|
||||
}
|
||||
|
||||
systemInactive: SystemPalette {
|
||||
|
@ -1,44 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import im.nheko 1.0
|
||||
|
||||
Switch {
|
||||
id: toggleButton
|
||||
|
||||
implicitWidth: indicatorItem.width
|
||||
|
||||
indicator: Item {
|
||||
id: indicatorItem
|
||||
|
||||
implicitWidth: 48
|
||||
implicitHeight: 24
|
||||
y: parent.height / 2 - height / 2
|
||||
|
||||
Rectangle {
|
||||
height: 3 * parent.height / 4
|
||||
radius: height / 2
|
||||
width: parent.width - height
|
||||
x: radius
|
||||
y: parent.height / 2 - height / 2
|
||||
color: toggleButton.checked ? "skyblue" : "grey"
|
||||
border.color: "#cccccc"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: toggleButton.checked ? parent.width - width : 0
|
||||
y: parent.height / 2 - height / 2
|
||||
width: parent.height
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: toggleButton.down ? "whitesmoke" : "whitesmoke"
|
||||
border.color: "#ebebeb"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
@ -18,12 +13,9 @@ Rectangle {
|
||||
z: 3
|
||||
color: colors.window
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: {
|
||||
TimelineManager.timeline.openRoomSettings();
|
||||
eventPoint.accepted = true;
|
||||
}
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: TimelineManager.openRoomSettings()
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
@ -61,7 +53,7 @@ Rectangle {
|
||||
height: avatarSize
|
||||
url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
|
||||
displayName: room ? room.roomName : qsTr("No room selected")
|
||||
onClicked: TimelineManager.timeline.openRoomSettings()
|
||||
onClicked: TimelineManager.openRoomSettings()
|
||||
}
|
||||
|
||||
Label {
|
||||
@ -73,7 +65,12 @@ Rectangle {
|
||||
text: room ? room.roomName : qsTr("No room selected")
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.RichText
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: TimelineManager.openRoomSettings()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
@ -95,30 +92,29 @@ Rectangle {
|
||||
image: ":/icons/icons/ui/vertical-ellipsis.png"
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Room options")
|
||||
onClicked: roomOptionsMenu.open(roomOptionsButton)
|
||||
onClicked: roomOptionsMenu.popup(roomOptionsButton)
|
||||
|
||||
Platform.Menu {
|
||||
Menu {
|
||||
id: roomOptionsMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false
|
||||
MenuItem {
|
||||
text: qsTr("Invite users")
|
||||
onTriggered: TimelineManager.openInviteUsersDialog()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
MenuItem {
|
||||
text: qsTr("Members")
|
||||
onTriggered: TimelineManager.openMemberListDialog()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
MenuItem {
|
||||
text: qsTr("Leave room")
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onTriggered: TimelineManager.timeline.openRoomSettings()
|
||||
onTriggered: TimelineManager.openRoomSettings()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./device-verification"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -14,21 +10,11 @@ ApplicationWindow {
|
||||
|
||||
property var profile
|
||||
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
height: 650
|
||||
width: 420
|
||||
minimumHeight: 420
|
||||
palette: colors
|
||||
color: colors.window
|
||||
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Dialog
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: userProfileDialog.close()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentL
|
||||
@ -44,86 +30,16 @@ ApplicationWindow {
|
||||
displayName: profile.displayName
|
||||
userid: profile.userid
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: profile.isSelf ? profile.changeAvatar() : TimelineManager.openImageOverlay(profile.avatarUrl, "")
|
||||
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
running: profile.isLoading
|
||||
visible: profile.isLoading
|
||||
}
|
||||
|
||||
Text {
|
||||
id: errorText
|
||||
|
||||
color: "red"
|
||||
visible: opacity > 0
|
||||
opacity: 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: hideErrorAnimation
|
||||
|
||||
running: false
|
||||
|
||||
PauseAnimation {
|
||||
duration: 4000
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: errorText
|
||||
property: 'opacity'
|
||||
to: 0
|
||||
duration: 1000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: profile
|
||||
onDisplayError: {
|
||||
errorText.text = errorMessage;
|
||||
errorText.opacity = 1;
|
||||
hideErrorAnimation.restart();
|
||||
}
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: displayUsername
|
||||
|
||||
property bool isUsernameEditingAllowed
|
||||
|
||||
readOnly: !isUsernameEditingAllowed
|
||||
Label {
|
||||
text: profile.displayName
|
||||
fontSizeMode: Text.HorizontalFit
|
||||
font.pixelSize: 20
|
||||
color: TimelineManager.userColor(profile.userid, colors.window)
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
selectByMouse: true
|
||||
onAccepted: {
|
||||
profile.changeUsername(displayUsername.text);
|
||||
displayUsername.isUsernameEditingAllowed = false;
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
visible: profile.isSelf
|
||||
anchors.leftMargin: 5
|
||||
anchors.left: displayUsername.right
|
||||
anchors.verticalCenter: displayUsername.verticalCenter
|
||||
image: displayUsername.isUsernameEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
|
||||
onClicked: {
|
||||
if (displayUsername.isUsernameEditingAllowed) {
|
||||
profile.changeUsername(displayUsername.text);
|
||||
displayUsername.isUsernameEditingAllowed = false;
|
||||
} else {
|
||||
displayUsername.isUsernameEditingAllowed = true;
|
||||
displayUsername.focus = true;
|
||||
displayUsername.selectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
@ -137,21 +53,32 @@ ApplicationWindow {
|
||||
|
||||
text: qsTr("Verify")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enabled: profile.userVerified != Crypto.Verified
|
||||
visible: profile.userVerified != Crypto.Verified && !profile.isSelf && profile.userVerificationEnabled
|
||||
enabled: !profile.isUserVerified
|
||||
visible: !profile.isUserVerified && !profile.isSelf && profile.userVerificationEnabled
|
||||
onClicked: profile.verify()
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : colors.buttonText)
|
||||
visible: profile.userVerified != Crypto.Unverified
|
||||
source: "image://colorimage/:/icons/icons/ui/lock.png?green"
|
||||
visible: profile.isUserVerified
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 8
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Ban the user")
|
||||
onClicked: profile.banUser()
|
||||
}
|
||||
// ImageButton{
|
||||
|
||||
// image:":/icons/icons/ui/volume-off-indicator.png"
|
||||
// Layout.margins: {
|
||||
// left: 5
|
||||
@ -163,10 +90,6 @@ ApplicationWindow {
|
||||
// profile.ignoreUser()
|
||||
// }
|
||||
// }
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 8
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/black-bubble-speech.png"
|
||||
hoverEnabled: true
|
||||
@ -181,16 +104,6 @@ ApplicationWindow {
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Kick the user")
|
||||
onClicked: profile.kickUser()
|
||||
visible: profile.room ? profile.room.permissions.canKick() : false
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Ban the user")
|
||||
onClicked: profile.banUser()
|
||||
visible: profile.room ? profile.room.permissions.canBan() : false
|
||||
}
|
||||
|
||||
}
|
||||
@ -242,7 +155,7 @@ ApplicationWindow {
|
||||
id: verifyButton
|
||||
|
||||
visible: (!profile.userVerificationEnabled && !profile.isSelf) || (profile.isSelf && (model.verificationStatus != VerificationStatus.VERIFIED || !profile.userVerificationEnabled))
|
||||
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
|
||||
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify"
|
||||
onClicked: {
|
||||
if (model.verificationStatus == VerificationStatus.VERIFIED)
|
||||
profile.unverify(model.deviceId);
|
||||
|
@ -1,8 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
@ -33,13 +29,9 @@ Item {
|
||||
fillMode: Image.Pad
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: TimelineManager.timeline.saveMedia(model.data.id)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.6
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width)
|
||||
property double tempHeight: tempWidth * model.data.proportionalHeight
|
||||
property double divisor: model.isReply ? 5 : 3
|
||||
property double divisor: model.isReply ? 4 : 2
|
||||
property bool tooHigh: tempHeight > timelineRoot.height / divisor
|
||||
|
||||
height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight)
|
||||
@ -36,24 +32,20 @@ Item {
|
||||
smooth: true
|
||||
mipmap: true
|
||||
|
||||
TapHandler {
|
||||
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
|
||||
onSingleTapped: {
|
||||
TimelineManager.openImageOverlay(model.data.url, model.data.id);
|
||||
eventPoint.accepted = true;
|
||||
}
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
|
||||
hoverEnabled: true
|
||||
anchors.fill: parent
|
||||
onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: overlay
|
||||
|
||||
anchors.fill: parent
|
||||
visible: mouseArea.hovered
|
||||
visible: mouseArea.containsMouse
|
||||
|
||||
Rectangle {
|
||||
id: container
|
||||
|
@ -1,14 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.6
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
property alias modelData: model.data
|
||||
property alias isReply: model.isReply
|
||||
property alias child: chooser.child
|
||||
property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
|
||||
|
||||
height: chooser.childrenRect.height
|
||||
|
@ -1,8 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
TextMessage {
|
||||
font.italic: true
|
||||
color: colors.buttonText
|
||||
height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined
|
||||
clip: isReply
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 2.1
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
|
||||
MatrixText {
|
||||
|
@ -1,10 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtMultimedia 5.6
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
@ -108,16 +103,45 @@ Rectangle {
|
||||
width: parent.width
|
||||
spacing: 15
|
||||
|
||||
ImageButton {
|
||||
Rectangle {
|
||||
id: button
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
//color: colors.window
|
||||
//radius: 22
|
||||
height: 32
|
||||
width: 32
|
||||
color: colors.window
|
||||
radius: 22
|
||||
height: 44
|
||||
width: 44
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
Image {
|
||||
id: img
|
||||
|
||||
anchors.centerIn: parent
|
||||
z: 3
|
||||
image: ":/icons/icons/ui/arrow-pointing-down.png"
|
||||
source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + colors.text
|
||||
fillMode: Image.Pad
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
switch (button.state) {
|
||||
case "":
|
||||
@ -135,29 +159,6 @@ Rectangle {
|
||||
break;
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: "stopped"
|
||||
|
||||
PropertyChanges {
|
||||
target: button
|
||||
image: ":/icons/icons/ui/play-sign.png"
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
|
||||
PropertyChanges {
|
||||
target: button
|
||||
image: ":/icons/icons/ui/pause-symbol.png"
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
CursorShape {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
@ -177,7 +178,7 @@ Rectangle {
|
||||
target: TimelineManager.timeline
|
||||
onMediaCached: {
|
||||
if (mxcUrl == model.data.url) {
|
||||
media.source = cacheUrl;
|
||||
media.source = "file://" + cacheUrl;
|
||||
button.state = "stopped";
|
||||
console.log("media loaded: " + mxcUrl + " at " + cacheUrl);
|
||||
}
|
||||
|
@ -1,8 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
@ -17,13 +13,10 @@ Item {
|
||||
width: parent.width
|
||||
height: replyContainer.height
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: chat.model.showEvent(modelData.id)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
preventStealing: false
|
||||
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
@ -50,9 +43,10 @@ Item {
|
||||
color: replyComponent.userColor
|
||||
textFormat: Text.RichText
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: chat.model.openUserProfile(reply.modelData.userId)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: chat.model.openUserProfile(reply.modelData.userId)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,18 +1,12 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import im.nheko 1.0
|
||||
|
||||
MatrixText {
|
||||
property string formatted: model.data.formattedBody
|
||||
property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body
|
||||
|
||||
text: "<style type=\"text/css\">a { color:" + colors.link + ";}\ncode { background-color: " + colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + colors.alternateBase + "'>")
|
||||
text: "<span style='white-space: pre-wrap'><style type=\"text/css\">a { color:" + colors.link + ";}\ncode { background-color: " + colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + colors.alternateBase + "'>") + "</span>"
|
||||
width: parent ? parent.width : undefined
|
||||
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
|
||||
clip: isReply
|
||||
selectByMouse: !Settings.mobileMode && !isReply
|
||||
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Window 2.10
|
||||
@ -15,12 +11,9 @@ ApplicationWindow {
|
||||
onClosing: TimelineManager.removeVerificationFlow(flow)
|
||||
title: stack.currentItem.title
|
||||
flags: Qt.Dialog
|
||||
modality: Qt.WindowModal
|
||||
palette: colors
|
||||
height: stack.implicitHeight
|
||||
width: stack.implicitWidth
|
||||
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
|
||||
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
|
||||
|
||||
StackView {
|
||||
id: stack
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.10
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.1
|
||||
@ -18,6 +14,5 @@ ImageButton {
|
||||
image: ":/icons/icons/ui/smile.png"
|
||||
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
|
||||
TimelineManager.queueReactionMessage(event_id, emoji);
|
||||
TimelineManager.focusMessageInput();
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.9
|
||||
@ -10,7 +6,7 @@ import QtQuick.Layouts 1.3
|
||||
import im.nheko 1.0
|
||||
import im.nheko.EmojiModel 1.0
|
||||
|
||||
Menu {
|
||||
Popup {
|
||||
id: emojiPopup
|
||||
|
||||
property var callback
|
||||
@ -24,8 +20,13 @@ Menu {
|
||||
|
||||
function show(showAt, callback) {
|
||||
console.debug("Showing emojiPicker");
|
||||
if (showAt) {
|
||||
parent = showAt;
|
||||
x = Math.round((showAt.width - width) / 2);
|
||||
y = showAt.height;
|
||||
}
|
||||
emojiPopup.callback = callback;
|
||||
popup(showAt ? showAt : null);
|
||||
open();
|
||||
}
|
||||
|
||||
margins: 0
|
||||
@ -35,74 +36,24 @@ Menu {
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
height: columnView.implicitHeight + 4
|
||||
width: columnView.implicitWidth
|
||||
|
||||
ColumnLayout {
|
||||
id: columnView
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
anchors.leftMargin: 3
|
||||
anchors.rightMargin: 3
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 2
|
||||
|
||||
// Search field
|
||||
TextField {
|
||||
id: emojiSearch
|
||||
|
||||
//width: gridView.width - 6
|
||||
Layout.topMargin: 3
|
||||
Layout.preferredWidth: 7 * 52 + 20 - 6
|
||||
placeholderText: qsTr("Search")
|
||||
selectByMouse: true
|
||||
rightPadding: clearSearch.width
|
||||
onTextChanged: searchTimer.restart()
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
forceActiveFocus();
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchTimer
|
||||
|
||||
interval: 350 // tweak as needed?
|
||||
onTriggered: {
|
||||
emojiPopup.model.searchString = emojiSearch.text;
|
||||
emojiPopup.model.category = Emoji.Category.Search;
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: clearSearch
|
||||
|
||||
visible: emojiSearch.text !== ''
|
||||
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
|
||||
focusPolicy: Qt.NoFocus
|
||||
onClicked: emojiSearch.clear()
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
right: parent.right
|
||||
}
|
||||
// clear the default hover effects.
|
||||
|
||||
background: Item {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Layout.bottomMargin: 0
|
||||
Layout.leftMargin: 3
|
||||
Layout.rightMargin: 3
|
||||
Layout.topMargin: 2
|
||||
|
||||
// emoji grid
|
||||
GridView {
|
||||
id: gridView
|
||||
|
||||
Layout.preferredHeight: cellHeight * 5
|
||||
Layout.preferredWidth: 7 * 52 + 20
|
||||
Layout.preferredHeight: emojiPopup.height
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: 4
|
||||
cellWidth: 52
|
||||
cellHeight: 52
|
||||
@ -152,6 +103,54 @@ Menu {
|
||||
|
||||
}
|
||||
|
||||
// Search field
|
||||
header: TextField {
|
||||
id: emojiSearch
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: emojiScroll.width + 4
|
||||
placeholderText: qsTr("Search")
|
||||
selectByMouse: true
|
||||
rightPadding: clearSearch.width
|
||||
onTextChanged: searchTimer.restart()
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
forceActiveFocus();
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchTimer
|
||||
|
||||
interval: 350 // tweak as needed?
|
||||
onTriggered: {
|
||||
emojiPopup.model.filter = emojiSearch.text;
|
||||
emojiPopup.model.category = EmojiCategory.Search;
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: clearSearch
|
||||
|
||||
visible: emojiSearch.text !== ''
|
||||
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
|
||||
focusPolicy: Qt.NoFocus
|
||||
onClicked: emojiSearch.clear()
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
right: parent.right
|
||||
}
|
||||
// clear the default hover effects.
|
||||
|
||||
background: Item {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: emojiScroll
|
||||
}
|
||||
@ -160,7 +159,6 @@ Menu {
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
visible: emojiSearch.text === ''
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: emojiPopup.colors.alternateBase
|
||||
@ -168,7 +166,6 @@ Menu {
|
||||
|
||||
// Category picker row
|
||||
RowLayout {
|
||||
visible: emojiSearch.text === ''
|
||||
Layout.bottomMargin: 0
|
||||
Layout.preferredHeight: 42
|
||||
implicitHeight: 42
|
||||
@ -181,42 +178,42 @@ Menu {
|
||||
// TODO: Would like to get 'simple' icons for the categories
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/people.png"
|
||||
category: Emoji.Category.People
|
||||
category: EmojiCategory.People
|
||||
}
|
||||
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/nature.png"
|
||||
category: Emoji.Category.Nature
|
||||
category: EmojiCategory.Nature
|
||||
}
|
||||
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/foods.png"
|
||||
category: Emoji.Category.Food
|
||||
category: EmojiCategory.Food
|
||||
}
|
||||
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/activity.png"
|
||||
category: Emoji.Category.Activity
|
||||
category: EmojiCategory.Activity
|
||||
}
|
||||
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/travel.png"
|
||||
category: Emoji.Category.Travel
|
||||
category: EmojiCategory.Travel
|
||||
}
|
||||
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/objects.png"
|
||||
category: Emoji.Category.Objects
|
||||
category: EmojiCategory.Objects
|
||||
}
|
||||
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/symbols.png"
|
||||
category: Emoji.Category.Symbols
|
||||
category: EmojiCategory.Symbols
|
||||
}
|
||||
|
||||
ListElement {
|
||||
image: ":/icons/icons/emoji-categories/flags.png"
|
||||
category: Emoji.Category.Flags
|
||||
category: EmojiCategory.Flags
|
||||
}
|
||||
|
||||
}
|
||||
@ -227,28 +224,27 @@ Menu {
|
||||
hoverEnabled: true
|
||||
ToolTip.text: {
|
||||
switch (model.category) {
|
||||
case Emoji.Category.People:
|
||||
case EmojiCategory.People:
|
||||
return qsTr('People');
|
||||
case Emoji.Category.Nature:
|
||||
case EmojiCategory.Nature:
|
||||
return qsTr('Nature');
|
||||
case Emoji.Category.Food:
|
||||
case EmojiCategory.Food:
|
||||
return qsTr('Food');
|
||||
case Emoji.Category.Activity:
|
||||
case EmojiCategory.Activity:
|
||||
return qsTr('Activity');
|
||||
case Emoji.Category.Travel:
|
||||
case EmojiCategory.Travel:
|
||||
return qsTr('Travel');
|
||||
case Emoji.Category.Objects:
|
||||
case EmojiCategory.Objects:
|
||||
return qsTr('Objects');
|
||||
case Emoji.Category.Symbols:
|
||||
case EmojiCategory.Symbols:
|
||||
return qsTr('Symbols');
|
||||
case Emoji.Category.Flags:
|
||||
case EmojiCategory.Flags:
|
||||
return qsTr('Flags');
|
||||
}
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
onClicked: {
|
||||
//emojiPopup.model.category = model.category;
|
||||
gridView.positionViewAtIndex(emojiPopup.model.sourceModel.categoryToIndex(model.category), GridView.Beginning);
|
||||
emojiPopup.model.category = model.category;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@ -279,6 +275,56 @@ Menu {
|
||||
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 1
|
||||
implicitWidth: 1
|
||||
height: parent.height
|
||||
color: emojiPopup.colors.alternateBase
|
||||
}
|
||||
|
||||
// Search Button is special
|
||||
AbstractButton {
|
||||
id: searchBtn
|
||||
|
||||
hoverEnabled: true
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.bottomMargin: 0
|
||||
ToolTip.text: qsTr("Search")
|
||||
ToolTip.visible: hovered
|
||||
onClicked: {
|
||||
// clear any filters
|
||||
emojiPopup.model.category = EmojiCategory.Search;
|
||||
gridView.positionViewAtBeginning();
|
||||
emojiSearch.forceActiveFocus();
|
||||
}
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
onPressed: mouse.accepted = false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
contentItem: Image {
|
||||
anchors.right: parent.right
|
||||
horizontalAlignment: Image.AlignHCenter
|
||||
verticalAlignment: Image.AlignVCenter
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
fillMode: Image.Pad
|
||||
smooth: true
|
||||
source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
@ -13,9 +9,9 @@ Item {
|
||||
property real radius: 0
|
||||
property color color: "#22000000"
|
||||
property real maxRadius: Math.max(width, height)
|
||||
readonly property real radiusAnimationRate: 0.05
|
||||
readonly property real radiusTailAnimationRate: 0.5
|
||||
readonly property real opacityAnimationDuration: 300
|
||||
property real radiusAnimationRate: 0.05
|
||||
property real radiusTailAnimationRate: 0.5
|
||||
property real opacityAnimationDuration: 300
|
||||
readonly property real diameter: radius * 2
|
||||
property real centerX
|
||||
property real centerY
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -16,7 +12,7 @@ Rectangle {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (CallManager.callType != CallType.VOICE)
|
||||
if (CallManager.isVideo)
|
||||
stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
|
||||
|
||||
}
|
||||
@ -46,46 +42,10 @@ Rectangle {
|
||||
}
|
||||
|
||||
Image {
|
||||
id: callTypeIcon
|
||||
|
||||
Layout.leftMargin: 4
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
}
|
||||
|
||||
Item {
|
||||
states: [
|
||||
State {
|
||||
name: "VOICE"
|
||||
when: CallManager.callType == CallType.VOICE
|
||||
|
||||
PropertyChanges {
|
||||
target: callTypeIcon
|
||||
source: "qrc:/icons/icons/ui/place-call.png"
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "VIDEO"
|
||||
when: CallManager.callType == CallType.VIDEO
|
||||
|
||||
PropertyChanges {
|
||||
target: callTypeIcon
|
||||
source: "qrc:/icons/icons/ui/video-call.png"
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "SCREEN"
|
||||
when: CallManager.callType == CallType.SCREEN
|
||||
|
||||
PropertyChanges {
|
||||
target: callTypeIcon
|
||||
source: "qrc:/icons/icons/ui/screen-share.png"
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
||||
}
|
||||
|
||||
Label {
|
||||
@ -143,7 +103,7 @@ Rectangle {
|
||||
|
||||
PropertyChanges {
|
||||
target: stackLayout
|
||||
currentIndex: CallManager.callType != CallType.VOICE ? 1 : 0
|
||||
currentIndex: CallManager.isVideo ? 1 : 0
|
||||
}
|
||||
|
||||
},
|
||||
@ -187,28 +147,20 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.leftMargin: 16
|
||||
visible: CallManager.callType == CallType.SCREEN && CallManager.callState == WebRTCState.CONNECTED
|
||||
text: qsTr("You are screen sharing")
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
color: "#000000"
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
visible: CallManager.haveLocalPiP
|
||||
visible: CallManager.haveLocalVideo
|
||||
width: 24
|
||||
height: 24
|
||||
buttonTextColor: "#000000"
|
||||
image: ":/icons/icons/ui/toggle-camera-view.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Hide/Show Picture-in-Picture")
|
||||
onClicked: CallManager.toggleLocalPiP()
|
||||
ToolTip.text: qsTr("Toggle camera view")
|
||||
onClicked: CallManager.toggleCameraView()
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
@ -44,7 +40,7 @@ Popup {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0
|
||||
visible: CallManager.isVideo && CallManager.cameras.length > 0
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 22
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -57,7 +53,7 @@ Popup {
|
||||
Layout.bottomMargin: msgView.height / 25
|
||||
|
||||
Image {
|
||||
property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
|
||||
property string image: CallManager.isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: msgView.height / 10
|
||||
@ -67,7 +63,7 @@ Popup {
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
|
||||
text: CallManager.isVideo ? qsTr("Video Call") : qsTr("Voice Call")
|
||||
font.pointSize: fontMetrics.font.pointSize * 2
|
||||
color: colors.windowText
|
||||
}
|
||||
@ -101,7 +97,7 @@ Popup {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0
|
||||
visible: CallManager.isVideo && CallManager.cameras.length > 0
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
Image {
|
||||
@ -163,7 +159,7 @@ Popup {
|
||||
RoundButton {
|
||||
id: acceptButton
|
||||
|
||||
property string image: CallManager.callType == CallType.VIDEO ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
|
||||
property string image: CallManager.isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"
|
||||
|
||||
implicitWidth: buttonLayout.buttonSize
|
||||
implicitHeight: buttonLayout.buttonSize
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -56,12 +52,12 @@ Rectangle {
|
||||
Layout.leftMargin: 4
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
||||
source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
||||
}
|
||||
|
||||
Label {
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
|
||||
text: CallManager.isVideo ? qsTr("Video Call") : qsTr("Voice Call")
|
||||
color: "#000000"
|
||||
}
|
||||
|
||||
@ -79,6 +75,7 @@ Rectangle {
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Devices")
|
||||
onClicked: {
|
||||
CallManager.refreshDevices();
|
||||
var dialog = devicesDialog.createObject(timelineRoot);
|
||||
dialog.open();
|
||||
}
|
||||
@ -86,7 +83,7 @@ Rectangle {
|
||||
|
||||
Button {
|
||||
Layout.rightMargin: 4
|
||||
icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
||||
icon.source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
||||
text: qsTr("Accept")
|
||||
palette: colors
|
||||
onClicked: {
|
||||
@ -105,7 +102,7 @@ Rectangle {
|
||||
dialog.open();
|
||||
return ;
|
||||
}
|
||||
if (CallManager.callType == CallType.VIDEO && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) {
|
||||
if (CallManager.isVideo && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) {
|
||||
var dialog = deviceError.createObject(timelineRoot, {
|
||||
"errorString": qsTr("Unknown camera: %1").arg(Settings.camera),
|
||||
"image": ":/icons/icons/ui/video-call.png"
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
@ -27,14 +23,6 @@ Popup {
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: screenShareDialog
|
||||
|
||||
ScreenShare {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
|
||||
@ -88,7 +76,7 @@ Popup {
|
||||
onClicked: {
|
||||
if (buttonLayout.validateMic()) {
|
||||
Settings.microphone = micCombo.currentText;
|
||||
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VOICE);
|
||||
CallManager.sendInvite(TimelineManager.timeline.roomId(), false);
|
||||
close();
|
||||
}
|
||||
}
|
||||
@ -102,23 +90,12 @@ Popup {
|
||||
if (buttonLayout.validateMic()) {
|
||||
Settings.microphone = micCombo.currentText;
|
||||
Settings.camera = cameraCombo.currentText;
|
||||
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VIDEO);
|
||||
CallManager.sendInvite(TimelineManager.timeline.roomId(), true);
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: CallManager.screenShareSupported
|
||||
text: qsTr("Screen")
|
||||
icon.source: "qrc:/icons/icons/ui/screen-share.png"
|
||||
onClicked: {
|
||||
var dialog = screenShareDialog.createObject(timelineRoot);
|
||||
dialog.open();
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: {
|
||||
|
@ -1,168 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
Popup {
|
||||
modal: true
|
||||
// only set the anchors on Qt 5.12 or higher
|
||||
// see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop
|
||||
Component.onCompleted: {
|
||||
if (anchors)
|
||||
anchors.centerIn = parent;
|
||||
|
||||
frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate);
|
||||
}
|
||||
palette: colors
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName)
|
||||
color: colors.windowText
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
Layout.bottomMargin: 8
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: qsTr("Window:")
|
||||
color: colors.windowText
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: windowCombo
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: CallManager.windowList()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
Layout.bottomMargin: 8
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: qsTr("Frame rate:")
|
||||
color: colors.windowText
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: frameRateCombo
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: ["25", "20", "15", "10", "5", "2", "1"]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: 10
|
||||
Layout.margins: 8
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Include your camera picture-in-picture")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
id: pipCheckBox
|
||||
|
||||
enabled: CallManager.cameras.length > 0
|
||||
checked: Settings.screenSharePiP
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Request remote camera")
|
||||
ToolTip.text: qsTr("View your callee's camera like a regular video call")
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
id: remoteVideoCheckBox
|
||||
|
||||
Layout.alignment: Qt.AlignRight
|
||||
checked: Settings.screenShareRemoteVideo
|
||||
ToolTip.text: qsTr("View your callee's camera like a regular video call")
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Hide mouse cursor")
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
id: hideCursorCheckBox
|
||||
|
||||
Layout.alignment: Qt.AlignRight
|
||||
checked: Settings.screenShareHideCursor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.margins: 8
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Share")
|
||||
icon.source: "qrc:/icons/icons/ui/screen-share.png"
|
||||
onClicked: {
|
||||
if (buttonLayout.validateMic()) {
|
||||
Settings.microphone = micCombo.currentText;
|
||||
if (pipCheckBox.checked)
|
||||
Settings.camera = cameraCombo.currentText;
|
||||
|
||||
Settings.screenShareFrameRate = frameRateCombo.currentText;
|
||||
Settings.screenSharePiP = pipCheckBox.checked;
|
||||
Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
|
||||
Settings.screenShareHideCursor = hideCursorCheckBox.checked;
|
||||
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex);
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Preview")
|
||||
onClicked: {
|
||||
CallManager.previewWindow(windowCombo.currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: colors.window
|
||||
border.color: colors.windowText
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,3 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import org.freedesktop.gstreamer.GLVideoItem 1.0
|
||||
|
||||
|
@ -74,7 +74,6 @@
|
||||
<file>icons/ui/end-call.png</file>
|
||||
<file>icons/ui/microphone-mute.png</file>
|
||||
<file>icons/ui/microphone-unmute.png</file>
|
||||
<file>icons/ui/screen-share.png</file>
|
||||
<file>icons/ui/toggle-camera-view.png</file>
|
||||
<file>icons/ui/video-call.png</file>
|
||||
|
||||
@ -129,22 +128,16 @@
|
||||
<file>qml/EncryptionIndicator.qml</file>
|
||||
<file>qml/ImageButton.qml</file>
|
||||
<file>qml/MatrixText.qml</file>
|
||||
<file>qml/MatrixTextField.qml</file>
|
||||
<file>qml/ToggleButton.qml</file>
|
||||
<file>qml/MessageInput.qml</file>
|
||||
<file>qml/MessageView.qml</file>
|
||||
<file>qml/NhekoBusyIndicator.qml</file>
|
||||
<file>qml/PrivacyScreen.qml</file>
|
||||
<file>qml/Reactions.qml</file>
|
||||
<file>qml/ReplyPopup.qml</file>
|
||||
<file>qml/ScrollHelper.qml</file>
|
||||
<file>qml/StatusIndicator.qml</file>
|
||||
<file>qml/TimelineRow.qml</file>
|
||||
<file>qml/TopBar.qml</file>
|
||||
<file>qml/QuickSwitcher.qml</file>
|
||||
<file>qml/ForwardCompleter.qml</file>
|
||||
<file>qml/TypingIndicator.qml</file>
|
||||
<file>qml/RoomSettings.qml</file>
|
||||
<file>qml/emoji/EmojiButton.qml</file>
|
||||
<file>qml/emoji/EmojiPicker.qml</file>
|
||||
<file>qml/UserProfile.qml</file>
|
||||
@ -171,7 +164,6 @@
|
||||
<file>qml/voip/CallInviteBar.qml</file>
|
||||
<file>qml/voip/DeviceError.qml</file>
|
||||
<file>qml/voip/PlaceCall.qml</file>
|
||||
<file>qml/voip/ScreenShare.qml</file>
|
||||
<file>qml/voip/VideoCall.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/media">
|
||||
|
@ -11,13 +11,26 @@ class Emoji(object):
|
||||
self.code = repr(code.encode('utf-8'))[1:].strip("'")
|
||||
self.shortname = shortname
|
||||
|
||||
def generate_code(emojis, category):
|
||||
tmpl = Template('''
|
||||
const std::vector<Emoji> emoji::Provider::{{ category }} = {
|
||||
// {{ category.capitalize() }}
|
||||
{%- for e in emoji %}
|
||||
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ category.capitalize() }}},
|
||||
{%- endfor %}
|
||||
};
|
||||
''')
|
||||
|
||||
d = dict(category=category, emoji=emojis)
|
||||
print(tmpl.render(d))
|
||||
|
||||
def generate_qml_list(**kwargs):
|
||||
tmpl = Template('''
|
||||
const QVector<Emoji> emoji::Provider::emoji = {
|
||||
{%- for c in kwargs.items() %}
|
||||
// {{ c[0].capitalize() }}
|
||||
{%- for e in c[1] %}
|
||||
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::Emoji::Category::{{ c[0].capitalize() }}},
|
||||
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ c[0].capitalize() }}},
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
};
|
||||
@ -80,4 +93,12 @@ if __name__ == '__main__':
|
||||
|
||||
# Use xclip to pipe the output to clipboard.
|
||||
# e.g ./codegen.py emoji.json | xclip -sel clip
|
||||
generate_code(people, 'people')
|
||||
generate_code(nature, 'nature')
|
||||
generate_code(food, 'food')
|
||||
generate_code(activity, 'activity')
|
||||
generate_code(travel, 'travel')
|
||||
generate_code(objects, 'objects')
|
||||
generate_code(symbols, 'symbols')
|
||||
generate_code(flags, 'flags')
|
||||
generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags)
|
@ -1,772 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# 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 2 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, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import errno
|
||||
import fnmatch
|
||||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from argparse import ArgumentParser
|
||||
from functools import reduce
|
||||
from urllib.parse import urljoin, urlparse, urlsplit, urlunparse, urlunsplit
|
||||
|
||||
import aiohttp
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
||||
|
||||
import gi
|
||||
gi.require_version('OSTree', '1.0')
|
||||
from gi.repository import Gio, GLib, OSTree
|
||||
|
||||
UPLOAD_CHUNK_LIMIT = 4 * 1024 * 1024
|
||||
DEFAULT_LIMIT = 2 ** 16
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
class UsageException(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
class ApiError(Exception):
|
||||
def __init__(self, response, body):
|
||||
self.url = str(response.url)
|
||||
self.status = response.status
|
||||
|
||||
try:
|
||||
self.body = json.loads(response);
|
||||
except:
|
||||
self.body = {"status": self.status, "error-type": "no-error", "message": "No json error details from server"}
|
||||
|
||||
def repr(self):
|
||||
return {
|
||||
"type": "api",
|
||||
"url": self.url,
|
||||
"status_code": self.status,
|
||||
"details": self.body
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return "Api call to %s failed with status %d, details: %s" % (self.url, self.status, self.body)
|
||||
|
||||
|
||||
# This is similar to the regular payload, but opens the file lazily
|
||||
class AsyncNamedFilePart(aiohttp.payload.Payload):
|
||||
def __init__(self,
|
||||
value,
|
||||
disposition='attachment',
|
||||
*args,
|
||||
**kwargs):
|
||||
self._file = None
|
||||
if 'filename' not in kwargs:
|
||||
kwargs['filename'] = os.path.basename(value)
|
||||
|
||||
super().__init__(value, *args, **kwargs)
|
||||
|
||||
if self._filename is not None and disposition is not None:
|
||||
self.set_content_disposition(disposition, filename=self._filename, quote_fields=False)
|
||||
|
||||
self._size = os.stat(value).st_size
|
||||
|
||||
async def write(self, writer):
|
||||
if self._file is None or self._file.closed:
|
||||
self._file = open(self._value, 'rb')
|
||||
try:
|
||||
chunk = self._file.read(DEFAULT_LIMIT)
|
||||
while chunk:
|
||||
await writer.write(chunk)
|
||||
chunk = self._file.read(DEFAULT_LIMIT)
|
||||
finally:
|
||||
self._file.close()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
def ostree_object_path(repo, obj):
|
||||
repodir = repo.get_path().get_path()
|
||||
return os.path.join(repodir, 'objects', obj[0:2], obj[2:])
|
||||
|
||||
def ostree_get_dir_files(repo, objects, dirtree):
|
||||
if dirtree.endswith(".dirtree"):
|
||||
dirtree = dirtree[:-8]
|
||||
dirtreev = repo.load_variant(OSTree.ObjectType.DIR_TREE, dirtree)[1]
|
||||
iter = OSTree.RepoCommitTraverseIter()
|
||||
iter.init_dirtree(repo, dirtreev, 0)
|
||||
while True:
|
||||
type = iter.next()
|
||||
if type == OSTree.RepoCommitIterResult.END:
|
||||
break
|
||||
if type == OSTree.RepoCommitIterResult.ERROR:
|
||||
break
|
||||
if type == OSTree.RepoCommitIterResult.FILE:
|
||||
d = iter.get_file()
|
||||
objects.add(d.out_checksum + ".filez")
|
||||
if type == OSTree.RepoCommitIterResult.DIR:
|
||||
pass
|
||||
|
||||
def local_needed_files(repo, metadata_objects):
|
||||
objects = set()
|
||||
for c in metadata_objects:
|
||||
if c.endswith(".dirtree"):
|
||||
ostree_get_dir_files(repo, objects, c)
|
||||
return objects
|
||||
|
||||
def local_needed_metadata_dirtree(repo, objects, dirtree_content, dirtree_meta):
|
||||
objects.add(dirtree_meta + ".dirmeta")
|
||||
dirtree_content_name = dirtree_content + ".dirtree"
|
||||
if dirtree_content_name in objects:
|
||||
return
|
||||
objects.add(dirtree_content_name)
|
||||
|
||||
dirtreev = repo.load_variant(OSTree.ObjectType.DIR_TREE, dirtree_content)[1]
|
||||
iter = OSTree.RepoCommitTraverseIter()
|
||||
iter.init_dirtree(repo, dirtreev, 0)
|
||||
while True:
|
||||
type = iter.next()
|
||||
if type == OSTree.RepoCommitIterResult.END:
|
||||
break
|
||||
if type == OSTree.RepoCommitIterResult.ERROR:
|
||||
break
|
||||
if type == OSTree.RepoCommitIterResult.FILE:
|
||||
pass
|
||||
if type == OSTree.RepoCommitIterResult.DIR:
|
||||
d = iter.get_dir()
|
||||
local_needed_metadata_dirtree(repo, objects, d.out_content_checksum, d.out_meta_checksum)
|
||||
|
||||
def local_needed_metadata(repo, commits):
|
||||
objects = set()
|
||||
for rev in commits:
|
||||
objects.add(rev + ".commit")
|
||||
commitv = repo.load_variant(OSTree.ObjectType.COMMIT, rev)[1]
|
||||
iter = OSTree.RepoCommitTraverseIter()
|
||||
iter.init_commit(repo, commitv, 0)
|
||||
while True:
|
||||
type = iter.next()
|
||||
if type == OSTree.RepoCommitIterResult.END:
|
||||
break
|
||||
if type == OSTree.RepoCommitIterResult.ERROR:
|
||||
break
|
||||
if type == OSTree.RepoCommitIterResult.FILE:
|
||||
pass
|
||||
if type == OSTree.RepoCommitIterResult.DIR:
|
||||
d = iter.get_dir()
|
||||
local_needed_metadata_dirtree(repo, objects, d.out_content_checksum, d.out_meta_checksum)
|
||||
return objects
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i:i + n]
|
||||
|
||||
async def missing_objects(session, build_url, token, wanted):
|
||||
missing=[]
|
||||
for chunk in chunks(wanted, 2000):
|
||||
wanted_json=json.dumps({'wanted': chunk}).encode('utf-8')
|
||||
data=gzip.compress(wanted_json)
|
||||
headers = {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Encoding': 'gzip',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
resp = await session.get(build_url + "/missing_objects", data=data, headers=headers)
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
data = await resp.json()
|
||||
missing.extend(data["missing"])
|
||||
return missing
|
||||
|
||||
async def upload_files(session, build_url, token, files):
|
||||
if len(files) == 0:
|
||||
return
|
||||
print("Uploading %d files (%d bytes)" % (len(files), reduce(lambda x, y: x + y, map(lambda f: f.size, files))))
|
||||
with aiohttp.MultipartWriter() as writer:
|
||||
for f in files:
|
||||
writer.append(f)
|
||||
writer.headers['Authorization'] = 'Bearer ' + token
|
||||
resp = await session.request("post", build_url + '/upload', data=writer, headers=writer.headers)
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
|
||||
async def upload_deltas(session, repo_path, build_url, token, deltas, refs, ignore_delta):
|
||||
if not len(deltas):
|
||||
return
|
||||
|
||||
req = []
|
||||
for ref, commit in refs.items():
|
||||
# Skip screenshots here
|
||||
parts = ref.split("/")
|
||||
if len(parts) == 4 and (parts[0] == "app" or parts[0] =="runtime") and not should_skip_delta(parts[1], ignore_delta):
|
||||
for delta in deltas:
|
||||
# Only upload from-scratch deltas, as these are the only reused ones
|
||||
if delta == commit:
|
||||
print(" %s: %s" % (ref, delta))
|
||||
delta_name = delta_name_encode (delta)
|
||||
delta_dir = repo_path + "/deltas/" + delta_name[:2] + "/" + delta_name[2:]
|
||||
parts = os.listdir(delta_dir)
|
||||
for part in parts:
|
||||
req.append(AsyncNamedFilePart(delta_dir + "/" + part, filename = delta_name + "." + part + ".delta"))
|
||||
|
||||
if len(req):
|
||||
await upload_files(session, build_url, token, req)
|
||||
|
||||
|
||||
async def upload_objects(session, repo_path, build_url, token, objects):
|
||||
req = []
|
||||
total_size = 0
|
||||
for file_obj in objects:
|
||||
named = get_object_multipart(repo_path, file_obj)
|
||||
file_size = named.size
|
||||
if total_size + file_size > UPLOAD_CHUNK_LIMIT: # The new object would bring us over the chunk limit
|
||||
if len(req) > 0: # We already have some objects, upload those first
|
||||
next_req = [named]
|
||||
total_size = file_size
|
||||
else:
|
||||
next_req = []
|
||||
req.append(named)
|
||||
total_size = 0
|
||||
await upload_files(session, build_url, token, req)
|
||||
req = next_req
|
||||
else:
|
||||
total_size = total_size + file_size
|
||||
req.append(named)
|
||||
|
||||
# Upload any remainder
|
||||
await upload_files(session, build_url, token, req)
|
||||
|
||||
async def create_ref(session, build_url, token, ref, commit):
|
||||
print("Creating ref %s with commit %s" % (ref, commit))
|
||||
resp = await session.post(build_url + "/build_ref", headers={'Authorization': 'Bearer ' + token}, json= { "ref": ref, "commit": commit} )
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
|
||||
data = await resp.json()
|
||||
return data
|
||||
|
||||
async def add_extra_ids(session, build_url, token, extra_ids):
|
||||
print("Adding extra ids %s" % (extra_ids))
|
||||
resp = await session.post(build_url + "/add_extra_ids", headers={'Authorization': 'Bearer ' + token}, json= { "ids": extra_ids} )
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
|
||||
data = await resp.json()
|
||||
return data
|
||||
|
||||
async def get_build(session, build_url, token):
|
||||
resp = await session.get(build_url, headers={'Authorization': 'Bearer ' + token})
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
data = await resp.json()
|
||||
return data
|
||||
|
||||
# For stupid reasons this is a string with json, lets expand it
|
||||
def reparse_job_results(job):
|
||||
job["results"] = json.loads(job.get("results", "{}"))
|
||||
return job
|
||||
|
||||
async def get_job(session, job_url, token):
|
||||
resp = await session.get(job_url, headers={'Authorization': 'Bearer ' + token}, json={})
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
data = await resp.json()
|
||||
return data
|
||||
|
||||
async def wait_for_job(session, job_url, token):
|
||||
reported_delay = False
|
||||
old_job_status = 0
|
||||
printed_len = 0
|
||||
iterations_since_change=0
|
||||
error_iterations = 0
|
||||
while True:
|
||||
try:
|
||||
resp = await session.get(job_url, headers={'Authorization': 'Bearer ' + token}, json={'log-offset': printed_len})
|
||||
async with resp:
|
||||
if resp.status == 200:
|
||||
error_iterations = 0
|
||||
job = await resp.json()
|
||||
job_status = job['status']
|
||||
if job_status == 0 and not reported_delay:
|
||||
reported_delay = True
|
||||
start_after_struct = job.get("start_after", None)
|
||||
if start_after_struct:
|
||||
start_after = start_after_struct.get("secs_since_epoch", None)
|
||||
now = time.time()
|
||||
if start_after and start_after > now:
|
||||
print("Waiting %d seconds before starting job" % (int(start_after - now)))
|
||||
if job_status > 0 and old_job_status == 0:
|
||||
print("/ Job was started");
|
||||
old_job_status = job_status
|
||||
log = job['log']
|
||||
if len(log) > 0:
|
||||
iterations_since_change=0
|
||||
for line in log.splitlines(True):
|
||||
print("| %s" % line, end="")
|
||||
printed_len = printed_len + len(log)
|
||||
else:
|
||||
iterations_since_change=iterations_since_change+1
|
||||
if job_status > 1:
|
||||
if job_status == 2:
|
||||
print("\ Job completed successfully")
|
||||
else:
|
||||
print("\ Job failed")
|
||||
return job
|
||||
else:
|
||||
iterations_since_change=4 # Start at 4 so we ramp up the delay faster
|
||||
error_iterations=error_iterations + 1
|
||||
if error_iterations <= 5:
|
||||
print("Unexpected response %s getting job log, ignoring" % resp.status)
|
||||
else:
|
||||
raise ApiError(resp, await resp.text())
|
||||
except OSError as e:
|
||||
if e.args[0] == errno.ECONNRESET:
|
||||
# Client disconnected, retry
|
||||
# Not sure exactly why, but i got a lot of ConnectionResetErrors here
|
||||
# in tests. I guess the server stops reusing a http2 session after a bit
|
||||
# Should be fine to retry with the backof
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
# Some polling backoff to avoid loading the server
|
||||
if iterations_since_change <= 1:
|
||||
sleep_time=1
|
||||
elif iterations_since_change < 5:
|
||||
sleep_time=3
|
||||
elif iterations_since_change < 15:
|
||||
sleep_time=5
|
||||
elif iterations_since_change < 30:
|
||||
sleep_time=10
|
||||
else:
|
||||
sleep_time=60
|
||||
time.sleep(sleep_time)
|
||||
|
||||
async def commit_build(session, build_url, eol, eol_rebase, token_type, wait, token):
|
||||
print("Committing build %s" % (build_url))
|
||||
json = {
|
||||
"endoflife": eol,
|
||||
"endoflife_rebase": eol_rebase
|
||||
}
|
||||
if token_type != None:
|
||||
json['token_type'] = token_type
|
||||
resp = await session.post(build_url + "/commit", headers={'Authorization': 'Bearer ' + token}, json=json)
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
|
||||
job = await resp.json()
|
||||
job_url = resp.headers['location'];
|
||||
|
||||
if wait:
|
||||
print("Waiting for commit job")
|
||||
job = await wait_for_job(session, job_url, token);
|
||||
|
||||
reparse_job_results(job)
|
||||
job["location"] = job_url
|
||||
return job
|
||||
|
||||
async def publish_build(session, build_url, wait, token):
|
||||
print("Publishing build %s" % (build_url))
|
||||
resp = await session.post(build_url + "/publish", headers={'Authorization': 'Bearer ' + token}, json= { } )
|
||||
async with resp:
|
||||
if resp.status == 400:
|
||||
body = await resp.text()
|
||||
try:
|
||||
msg = json.loads(body)
|
||||
if msg.get("current-state", "") == "published":
|
||||
print("the build has been already published")
|
||||
return {}
|
||||
except:
|
||||
pass
|
||||
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
|
||||
job = await resp.json()
|
||||
job_url = resp.headers['location'];
|
||||
|
||||
if wait:
|
||||
print("Waiting for publish job")
|
||||
job = await wait_for_job(session, job_url, token);
|
||||
|
||||
reparse_job_results(job)
|
||||
job["location"] = job_url
|
||||
return job
|
||||
|
||||
async def purge_build(session, build_url, token):
|
||||
print("Purging build %s" % (build_url))
|
||||
resp = await session.post(build_url + "/purge", headers={'Authorization': 'Bearer ' + token}, json= {} )
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
return await resp.json()
|
||||
|
||||
async def create_token(session, manager_url, token, name, subject, scope, duration):
|
||||
token_url = urljoin(manager_url, "api/v1/token_subset")
|
||||
resp = await session.post(token_url, headers={'Authorization': 'Bearer ' + token}, json = {
|
||||
"name": name,
|
||||
"sub": subject,
|
||||
"scope": scope,
|
||||
"duration": duration,
|
||||
})
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
return await resp.json()
|
||||
|
||||
def get_object_multipart(repo_path, object):
|
||||
return AsyncNamedFilePart(repo_path + "/objects/" + object[:2] + "/" + object[2:], filename=object)
|
||||
|
||||
async def create_command(session, args):
|
||||
build_url = urljoin(args.manager_url, "api/v1/build")
|
||||
resp = await session.post(build_url, headers={'Authorization': 'Bearer ' + args.token}, json={
|
||||
"repo": args.repo
|
||||
})
|
||||
async with resp:
|
||||
if resp.status != 200:
|
||||
raise ApiError(resp, await resp.text())
|
||||
data = await resp.json()
|
||||
data["location"] = resp.headers['location']
|
||||
if not args.print_output:
|
||||
print(resp.headers['location'])
|
||||
return data
|
||||
|
||||
def delta_name_part_encode(commit):
|
||||
return base64.b64encode(binascii.unhexlify(commit), b"+_")[:-1].decode("utf-8")
|
||||
|
||||
def delta_name_encode (delta):
|
||||
return "-".join(map(delta_name_part_encode, delta.split("-")))
|
||||
|
||||
def should_skip_delta(id, globs):
|
||||
if globs:
|
||||
for glob in globs:
|
||||
if fnmatch.fnmatch(id, glob):
|
||||
return True
|
||||
return False
|
||||
|
||||
def build_url_to_api(build_url):
|
||||
parts = urlparse(build_url)
|
||||
path = os.path.dirname(os.path.dirname(parts.path))
|
||||
return urlunparse((parts.scheme, parts.netloc, path, None, None, None))
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(6),
|
||||
wait=wait_fixed(10),
|
||||
retry=retry_if_exception_type(ApiError),
|
||||
reraise=True,
|
||||
)
|
||||
async def push_command(session, args):
|
||||
local_repo = OSTree.Repo.new(Gio.File.new_for_path(args.repo_path))
|
||||
try:
|
||||
local_repo.open(None)
|
||||
except GLib.Error as err:
|
||||
raise UsageException("Can't open repo %s: %s" % (args.repo_path, err.message)) from err
|
||||
|
||||
refs = {}
|
||||
if len(args.branches) == 0:
|
||||
_, all_refs = local_repo.list_refs(None, None)
|
||||
for ref in all_refs:
|
||||
if ref.startswith("app/") or ref.startswith("runtime/") or ref.startswith("screenshots/"):
|
||||
refs[ref] = all_refs[ref]
|
||||
else:
|
||||
for branch in args.branches:
|
||||
_, rev = local_repo.resolve_rev(branch, False)
|
||||
refs[branch] = rev
|
||||
|
||||
if (args.minimal_token):
|
||||
id = os.path.basename(urlparse(args.build_url).path)
|
||||
token = create_token(args.build_url, args.token, "minimal-upload", "build/%s" % (id), ["upload"], 60*60)["token"]
|
||||
else:
|
||||
token = args.token
|
||||
|
||||
print("Uploading refs to %s: %s"% (args.build_url, list(refs)))
|
||||
|
||||
metadata_objects = local_needed_metadata(local_repo, refs.values())
|
||||
|
||||
print("Refs contain %d metadata objects" % (len(metadata_objects)))
|
||||
|
||||
missing_metadata_objects = await missing_objects(session, args.build_url, token, list(metadata_objects))
|
||||
|
||||
print("Remote missing %d of those" % (len(missing_metadata_objects)))
|
||||
|
||||
file_objects = local_needed_files(local_repo, missing_metadata_objects)
|
||||
print("Has %d file objects for those" % (len(file_objects)))
|
||||
|
||||
missing_file_objects = await missing_objects(session, args.build_url, token, list(file_objects))
|
||||
print("Remote missing %d of those" % (len(missing_file_objects)))
|
||||
|
||||
# First upload all missing file objects
|
||||
print("Uploading file objects")
|
||||
await upload_objects(session, args.repo_path, args.build_url, token, missing_file_objects)
|
||||
|
||||
# Then all the metadata
|
||||
print("Uploading metadata objects")
|
||||
await upload_objects(session, args.repo_path, args.build_url, token, missing_metadata_objects)
|
||||
|
||||
_, deltas = local_repo.list_static_delta_names()
|
||||
print("Uploading deltas")
|
||||
await upload_deltas(session, args.repo_path, args.build_url, token, deltas, refs, args.ignore_delta)
|
||||
|
||||
# Then the refs
|
||||
for ref, commit in refs.items():
|
||||
await create_ref(session, args.build_url, token, ref, commit)
|
||||
|
||||
# Then any extra ids
|
||||
if args.extra_id:
|
||||
await add_extra_ids(session, args.build_url, token, args.extra_id)
|
||||
|
||||
commit_job = None
|
||||
publish_job = None
|
||||
update_job = None
|
||||
|
||||
# Note, this always uses the full token, as the minimal one only has upload permissions
|
||||
if args.commit or args.publish:
|
||||
commit_job = await commit_build(session, args.build_url, args.end_of_life, args.end_of_life_rebase, args.token_type, args.publish or args.wait, args.token)
|
||||
|
||||
if args.publish:
|
||||
publish_job = await publish_build(session, args.build_url, args.wait or args.wait_update, args.token)
|
||||
update_job_id = publish_job.get("results", {}).get("update-repo-job", None)
|
||||
if update_job_id:
|
||||
print("Queued repo update job %d" %(update_job_id))
|
||||
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
|
||||
if args.wait_update:
|
||||
print("Waiting for repo update job")
|
||||
update_job = await wait_for_job (session, update_job_url, token);
|
||||
else:
|
||||
update_job = await get_job(session, update_job_url, token)
|
||||
reparse_job_results(update_job)
|
||||
update_job["location"] = update_job_url
|
||||
|
||||
data = await get_build(session, args.build_url, args.token)
|
||||
if commit_job:
|
||||
data["commit_job"] = commit_job
|
||||
if publish_job:
|
||||
data["publish_job"] = publish_job
|
||||
if update_job:
|
||||
data["update_job"] = update_job
|
||||
return data
|
||||
|
||||
async def commit_command(session, args):
|
||||
job = await commit_build(session, args.build_url, args.end_of_life, args.end_of_life_rebase, args.token_type, args.wait, args.token)
|
||||
return job
|
||||
|
||||
async def publish_command(session, args):
|
||||
job = await publish_build(session, args.build_url, args.wait or args.wait_update, args.token)
|
||||
update_job_id = job.get("results", {}).get("update-repo-job", None)
|
||||
if update_job_id:
|
||||
print("Queued repo update job %d" %(update_job_id))
|
||||
update_job_url = build_url_to_api(args.build_url) + "/job/" + str(update_job_id)
|
||||
if args.wait_update:
|
||||
print("Waiting for repo update job")
|
||||
update_job = await wait_for_job(session, update_job_url, args.token);
|
||||
else:
|
||||
update_job = await get_job(session, update_job_url, args.token)
|
||||
reparse_job_results(update_job)
|
||||
update_job["location"] = update_job_url
|
||||
return job
|
||||
|
||||
async def purge_command(session, args):
|
||||
job = await purge_build(session, args.build_url, args.token)
|
||||
return job
|
||||
|
||||
async def create_token_command(session, args):
|
||||
data = await create_token(session, args.manager_url, args.token, args.name, args.subject, args.scope, args.duration)
|
||||
if not args.print_output:
|
||||
print(data['token'])
|
||||
return data
|
||||
|
||||
async def follow_job_command(session, args):
|
||||
job = await wait_for_job(session, args.job_url, args.token)
|
||||
return job
|
||||
|
||||
async def run_with_session(args):
|
||||
timeout = aiohttp.ClientTimeout(total=90*60)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
result = await args.func(session, args)
|
||||
return result
|
||||
|
||||
if __name__ == '__main__':
|
||||
progname = os.path.basename(sys.argv[0])
|
||||
|
||||
parser = ArgumentParser(prog=progname)
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='enable verbose output')
|
||||
parser.add_argument('--debug', action='store_true',
|
||||
help='enable debugging output')
|
||||
parser.add_argument('--output', help='Write output json to file')
|
||||
parser.add_argument('--print-output', action='store_true', help='Print output json')
|
||||
parser.add_argument('--token', help='use this token')
|
||||
parser.add_argument('--token-file', help='use token from file')
|
||||
subparsers = parser.add_subparsers(title='subcommands',
|
||||
dest='subparser_name',
|
||||
description='valid subcommands',
|
||||
help='additional help')
|
||||
|
||||
create_parser = subparsers.add_parser('create', help='Create new build')
|
||||
create_parser.add_argument('manager_url', help='remote repo manager url')
|
||||
create_parser.add_argument('repo', help='repo name')
|
||||
create_parser.set_defaults(func=create_command)
|
||||
|
||||
push_parser = subparsers.add_parser('push', help='Push to repo manager')
|
||||
push_parser.add_argument('build_url', help='remote build url')
|
||||
push_parser.add_argument('repo_path', help='local repository')
|
||||
push_parser.add_argument('branches', nargs='*', help='branches to push')
|
||||
push_parser.add_argument('--commit', action='store_true',
|
||||
help='commit build after pushing')
|
||||
push_parser.add_argument('--publish', action='store_true',
|
||||
help='publish build after committing')
|
||||
push_parser.add_argument('--extra-id', action='append', help='add extra collection-id')
|
||||
push_parser.add_argument('--ignore-delta', action='append', help='don\'t upload deltas matching this glob')
|
||||
push_parser.add_argument('--wait', action='store_true',
|
||||
help='wait for commit/publish to finish')
|
||||
push_parser.add_argument('--wait-update', action='store_true',
|
||||
help='wait for update-repo to finish')
|
||||
push_parser.add_argument('--minimal-token', action='store_true',
|
||||
help='Create minimal token for the upload')
|
||||
push_parser.add_argument('--end-of-life', help='Set end of life')
|
||||
push_parser.add_argument('--end-of-life-rebase', help='Set new ID which will supercede the current one')
|
||||
push_parser.add_argument('--token-type', help='Set token type', type=int)
|
||||
push_parser.set_defaults(func=push_command)
|
||||
|
||||
commit_parser = subparsers.add_parser('commit', help='Commit build')
|
||||
commit_parser.add_argument('--wait', action='store_true',
|
||||
help='wait for commit to finish')
|
||||
commit_parser.add_argument('--end-of-life', help='Set end of life')
|
||||
commit_parser.add_argument('--end-of-life-rebase', help='Set new ID which will supercede the current one')
|
||||
commit_parser.add_argument('--token-type', help='Set token type', type=int)
|
||||
commit_parser.add_argument('build_url', help='remote build url')
|
||||
commit_parser.set_defaults(func=commit_command)
|
||||
|
||||
publish_parser = subparsers.add_parser('publish', help='Publish build')
|
||||
publish_parser.add_argument('--wait', action='store_true',
|
||||
help='wait for publish to finish')
|
||||
publish_parser.add_argument('--wait-update', action='store_true',
|
||||
help='wait for update-repo to finish')
|
||||
publish_parser.add_argument('build_url', help='remote build url')
|
||||
publish_parser.set_defaults(func=publish_command)
|
||||
|
||||
purge_parser = subparsers.add_parser('purge', help='Purge build')
|
||||
purge_parser.add_argument('build_url', help='remote build url')
|
||||
purge_parser.set_defaults(func=purge_command)
|
||||
|
||||
create_token_parser = subparsers.add_parser('create-token', help='Create subset token')
|
||||
create_token_parser.add_argument('manager_url', help='remote repo manager url')
|
||||
create_token_parser.add_argument('name', help='Name')
|
||||
create_token_parser.add_argument('subject', help='Subject')
|
||||
create_token_parser.add_argument('scope', nargs='*', help='Scope')
|
||||
create_token_parser.add_argument('--duration', help='Duration until expires, in seconds',
|
||||
default=60*60*24, # Default duration is one day
|
||||
type=int)
|
||||
create_token_parser.set_defaults(func=create_token_command)
|
||||
|
||||
follow_job_parser = subparsers.add_parser('follow-job', help='Follow existing job log')
|
||||
follow_job_parser.add_argument('job_url', help='url of job')
|
||||
follow_job_parser.set_defaults(func=follow_job_command)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
loglevel = logging.WARNING
|
||||
if args.verbose:
|
||||
loglevel = logging.INFO
|
||||
if args.debug:
|
||||
loglevel = logging.DEBUG
|
||||
|
||||
logging.basicConfig(format='%(module)s: %(levelname)s: %(message)s',
|
||||
level=loglevel, stream=sys.stderr)
|
||||
|
||||
if not args.subparser_name:
|
||||
print("No subcommand specified, see --help for usage")
|
||||
exit(1)
|
||||
|
||||
if not args.token:
|
||||
if args.token_file:
|
||||
file = open(args.token_file, 'rb')
|
||||
args.token = file.read().splitlines()[0].decode("utf-8").strip()
|
||||
elif "REPO_TOKEN" in os.environ:
|
||||
args.token = os.environ["REPO_TOKEN"]
|
||||
else:
|
||||
print("No token available, pass with --token, --token-file or $REPO_TOKEN")
|
||||
exit(1)
|
||||
|
||||
|
||||
res = 1
|
||||
output = None
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
result = loop.run_until_complete(run_with_session(args))
|
||||
|
||||
output = {
|
||||
"command": args.subparser_name,
|
||||
"result": result,
|
||||
}
|
||||
res = 0
|
||||
except SystemExit:
|
||||
# Something called sys.exit(), lets just exit
|
||||
res = 1
|
||||
raise # Pass on regular exit callse
|
||||
except ApiError as e:
|
||||
eprint(str(e))
|
||||
output = {
|
||||
"command": args.subparser_name,
|
||||
"error": e.repr(),
|
||||
}
|
||||
except UsageException as e:
|
||||
eprint(str(e))
|
||||
output = {
|
||||
"error": {
|
||||
"type": "usage",
|
||||
"details": {
|
||||
"message": str(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
except:
|
||||
ei = sys.exc_info()
|
||||
eprint("Unexpected %s exception in %s: %s" % (ei[0].__name__, args.subparser_name, ei[1]))
|
||||
eprint(traceback.format_exc())
|
||||
output = {
|
||||
"command": args.subparser_name,
|
||||
"error": {
|
||||
"type": "exception",
|
||||
"details": {
|
||||
"error-type": ei[0].__name__,
|
||||
"message": str(ei[1]),
|
||||
}
|
||||
}
|
||||
}
|
||||
res = 1
|
||||
|
||||
if output:
|
||||
if args.print_output:
|
||||
print(json.dumps(output, indent=4))
|
||||
if args.output:
|
||||
f = open(args.output,"w+")
|
||||
f.write(json.dumps(output, indent=4))
|
||||
f.write("\n")
|
||||
f.close()
|
||||
exit(res)
|
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Missing repo to upload!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${CI_COMMIT_TAG}" ]; then
|
||||
BUILD_URL=$(./flat-manager-client create https://flatpak.neko.dev stable)
|
||||
elif [ "master" = "${CI_COMMIT_REF_NAME}" ]; then
|
||||
BUILD_URL=$(./flat-manager-client create https://flatpak.neko.dev nightly)
|
||||
fi
|
||||
|
||||
if [ -z "${BUILD_URL}" ]; then
|
||||
echo "No upload to repo."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BUILD_URL=${BUILD_URL/http:/https:}
|
||||
|
||||
./flat-manager-client push $BUILD_URL $1
|
||||
./flat-manager-client commit --wait $BUILD_URL
|
||||
./flat-manager-client publish --wait $BUILD_URL
|
||||
|
@ -1,11 +1,22 @@
|
||||
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
/*
|
||||
* 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 <QBuffer>
|
||||
#include <QPixmapCache>
|
||||
#include <QPointer>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
@ -13,14 +24,13 @@
|
||||
#include "Cache.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "MxcImageProvider.h"
|
||||
#include "Utils.h"
|
||||
|
||||
static QPixmapCache avatar_cache;
|
||||
|
||||
namespace AvatarProvider {
|
||||
void
|
||||
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
|
||||
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback)
|
||||
{
|
||||
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
|
||||
|
||||
@ -35,31 +45,43 @@ resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
|
||||
return;
|
||||
}
|
||||
|
||||
MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
|
||||
QSize(size, size),
|
||||
[callback, cacheKey, recv = QPointer<QObject>(receiver)](
|
||||
QString, QSize, QImage img, QString) {
|
||||
if (!recv)
|
||||
auto data = cache::image(cacheKey);
|
||||
if (!data.isNull()) {
|
||||
pixmap = QPixmap::fromImage(utils::readImage(&data));
|
||||
avatar_cache.insert(cacheKey, pixmap);
|
||||
callback(pixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
auto proxy = std::make_shared<AvatarProxy>();
|
||||
QObject::connect(proxy.get(),
|
||||
&AvatarProxy::avatarDownloaded,
|
||||
recv,
|
||||
[callback, cacheKey](QPixmap pm) {
|
||||
if (!pm.isNull())
|
||||
avatar_cache.insert(
|
||||
cacheKey, pm);
|
||||
receiver,
|
||||
[callback, cacheKey](QByteArray data) {
|
||||
QPixmap pm = QPixmap::fromImage(utils::readImage(&data));
|
||||
avatar_cache.insert(cacheKey, pm);
|
||||
callback(pm);
|
||||
});
|
||||
|
||||
if (img.isNull()) {
|
||||
emit proxy->avatarDownloaded(QPixmap{});
|
||||
return;
|
||||
mtx::http::ThumbOpts opts;
|
||||
opts.width = size;
|
||||
opts.height = size;
|
||||
opts.mxc_url = avatarUrl.toStdString();
|
||||
|
||||
http::client()->get_thumbnail(
|
||||
opts,
|
||||
[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,
|
||||
mtx::errors::to_string(err->matrix_error.errcode),
|
||||
err->matrix_error.error);
|
||||
} else {
|
||||
cache::saveImage(cacheKey.toStdString(), res);
|
||||
}
|
||||
|
||||
auto pm = QPixmap::fromImage(std::move(img));
|
||||
emit proxy->avatarDownloaded(pm);
|
||||
emit proxy->avatarDownloaded(QByteArray(res.data(), (int)res.size()));
|
||||
});
|
||||
}
|
||||
|
||||
@ -70,8 +92,8 @@ resolve(const QString &room_id,
|
||||
QObject *receiver,
|
||||
AvatarCallback callback)
|
||||
{
|
||||
auto avatarUrl = cache::avatarUrl(room_id, user_id);
|
||||
const auto avatarUrl = cache::avatarUrl(room_id, user_id);
|
||||
|
||||
resolve(std::move(avatarUrl), size, receiver, callback);
|
||||
resolve(avatarUrl, size, receiver, callback);
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
/*
|
||||
* 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 <QPixmap>
|
||||
#include <functional>
|
||||
|
||||
using AvatarCallback = std::function<void(QPixmap)>;
|
||||
|
||||
class AvatarProxy : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void avatarDownloaded(QPixmap pm);
|
||||
void avatarDownloaded(const QByteArray &data);
|
||||
};
|
||||
|
||||
using AvatarCallback = std::function<void(QPixmap)>;
|
||||
|
||||
namespace AvatarProvider {
|
||||
void
|
||||
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback cb);
|
||||
resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb);
|
||||
void
|
||||
resolve(const QString &room_id,
|
||||
const QString &user_id,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user