Update video_player_enhancements with changes from master

This commit is contained in:
Joseph Donofry 2021-11-03 21:43:11 -04:00
commit 743a83c8e6
No known key found for this signature in database
GPG Key ID: E8A1D78EF044B0CB
265 changed files with 53777 additions and 34019 deletions

73
.ci/macos/notarize.sh Executable file
View File

@ -0,0 +1,73 @@
#!/bin/sh
set -u
# Modified version of script found at:
# https://forum.qt.io/topic/96652/how-to-notarize-qt-application-on-macos/18
# Add Qt binaries to path
PATH="/usr/local/opt/qt@5/bin/:${PATH}"
security unlock-keychain -p "${RUNNER_USER_PW}" login.keychain
( cd build || exit
# macdeployqt does not copy symlinks over.
# this specifically addresses icu4c issues but nothing else.
# We might not even need this any longer...
# ICU_LIB="$(brew --prefix icu4c)/lib"
# export ICU_LIB
# mkdir -p nheko.app/Contents/Frameworks
# find "${ICU_LIB}" -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true
macdeployqt nheko.app -dmg -always-overwrite -qmldir=../resources/qml/ -sign-for-notarization="${APPLE_DEV_IDENTITY}"
user=$(id -nu)
chown "${user}" nheko.dmg
)
NOTARIZE_SUBMIT_LOG=$(mktemp -t notarize-submit)
NOTARIZE_STATUS_LOG=$(mktemp -t notarize-status)
finish() {
rm "$NOTARIZE_SUBMIT_LOG" "$NOTARIZE_STATUS_LOG"
}
trap finish EXIT
dmgbuild -s .ci/macos/settings.json "Nheko" nheko.dmg
codesign -s "${APPLE_DEV_IDENTITY}" nheko.dmg
user=$(id -nu)
chown "${user}" nheko.dmg
echo "--> Start Notarization process"
xcrun altool -t osx -f nheko.dmg --primary-bundle-id "io.github.nheko-reborn.nheko" --notarize-app -u "${APPLE_DEV_USER}" -p "${APPLE_DEV_PASS}" > "$NOTARIZE_SUBMIT_LOG" 2>&1
requestUUID="$(awk -F ' = ' '/RequestUUID/ {print $2}' "$NOTARIZE_SUBMIT_LOG")"
while sleep 60 && date; do
echo "--> Checking notarization status for ${requestUUID}"
xcrun altool --notarization-info "${requestUUID}" -u "${APPLE_DEV_USER}" -p "${APPLE_DEV_PASS}" > "$NOTARIZE_STATUS_LOG" 2>&1
isSuccess=$(grep "success" "$NOTARIZE_STATUS_LOG")
isFailure=$(grep "invalid" "$NOTARIZE_STATUS_LOG")
if [ -n "${isSuccess}" ]; then
echo "Notarization done!"
xcrun stapler staple -v nheko.dmg
echo "Stapler done!"
break
fi
if [ -n "${isFailure}" ]; then
echo "Notarization failed"
cat "$NOTARIZE_STATUS_LOG" 1>&2
return 1
fi
echo "Notarization not finished yet, sleep 1m then check again..."
done
VERSION=${CI_COMMIT_SHORT_SHA}
if [ -n "$VERSION" ]; then
mv nheko.dmg "nheko-${VERSION}.dmg"
mkdir artifacts
cp "nheko-${VERSION}.dmg" artifacts/
fi

View File

@ -1,14 +1,14 @@
--- ---
Language: Cpp Language: Cpp
Standard: Cpp11 Standard: c++17
AccessModifierOffset: -8 AccessModifierOffset: -4
AlignAfterOpenBracket: Align AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: true AlignConsecutiveAssignments: true
AllowShortFunctionsOnASingleLine: true AllowShortFunctionsOnASingleLine: true
BasedOnStyle: Mozilla BasedOnStyle: Mozilla
ColumnLimit: 100 ColumnLimit: 100
IndentCaseLabels: false IndentCaseLabels: false
IndentWidth: 8 IndentWidth: 4
KeepEmptyLinesAtTheStartOfBlocks: false KeepEmptyLinesAtTheStartOfBlocks: false
PointerAlignment: Right PointerAlignment: Right
Cpp11BracedListStyle: true Cpp11BracedListStyle: true

View File

@ -55,7 +55,6 @@ build-macos:
#- brew update #- brew update
#- brew reinstall --force python3 #- brew reinstall --force python3
#- brew bundle --file=./.ci/macos/Brewfile --force --cleanup #- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
- pip3 install dmgbuild
- rm -rf ../.hunter && mv .hunter ../.hunter || true - rm -rf ../.hunter && mv .hunter ../.hunter || true
script: script:
- export PATH=/usr/local/opt/qt@5/bin/:${PATH} - export PATH=/usr/local/opt/qt@5/bin/:${PATH}
@ -72,19 +71,40 @@ build-macos:
- cmake --build build - cmake --build build
after_script: after_script:
- mv ../.hunter .hunter - mv ../.hunter .hunter
- ./.ci/macos/deploy.sh
- ./.ci/upload-nightly-gitlab.sh artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
artifacts: artifacts:
paths: paths:
- artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg - build/nheko.app
name: nheko-${CI_COMMIT_SHORT_SHA}-macos name: nheko-${CI_COMMIT_SHORT_SHA}-macos-app
expose_as: 'macos-dmg' expose_as: 'macos-app'
public: false
cache: cache:
key: "${CI_JOB_NAME}" key: "${CI_JOB_NAME}"
paths: paths:
- .hunter/ - .hunter/
- "${CCACHE_DIR}" - "${CCACHE_DIR}"
codesign-macos:
stage: deploy
tags: [macos]
before_script:
- pip3 install dmgbuild
script:
- export PATH=/usr/local/opt/qt@5/bin/:${PATH}
- ./.ci/macos/notarize.sh
after_script:
- ./.ci/upload-nightly-gitlab.sh artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
needs:
- build-macos
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
- if : $CI_COMMIT_TAG
artifacts:
paths:
- artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
name: nheko-${CI_COMMIT_SHORT_SHA}-macos
expose_as: 'macos-dmg'
build-flatpak-amd64: build-flatpak-amd64:
stage: build stage: build
image: ubuntu:latest image: ubuntu:latest
@ -171,7 +191,7 @@ appimage-amd64:
- apt-get install -y git wget curl - apt-get install -y git wget curl
# update appimage-builder (optional) # update appimage-builder (optional)
- pip3 install --upgrade git+https://www.opencode.net/azubieta/appimagecraft.git - pip3 install --upgrade git+https://github.com/AppImageCrafters/appimage-builder.git
- 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 libssl-dev git ninja-build qt5keychain-dev libgtest-dev ccache libevent-dev libcurl4-openssl-dev libgl1-mesa-dev - 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 libssl-dev git ninja-build qt5keychain-dev libgtest-dev ccache libevent-dev libcurl4-openssl-dev libgl1-mesa-dev
- wget https://github.com/Kitware/CMake/releases/download/v3.19.0/cmake-3.19.0-Linux-x86_64.sh && sh cmake-3.19.0-Linux-x86_64.sh --skip-license --prefix=/usr/local - wget https://github.com/Kitware/CMake/releases/download/v3.19.0/cmake-3.19.0-Linux-x86_64.sh && sh cmake-3.19.0-Linux-x86_64.sh --skip-license --prefix=/usr/local

View File

@ -281,8 +281,6 @@ set(SRC_FILES
src/dialogs/CreateRoom.cpp src/dialogs/CreateRoom.cpp
src/dialogs/FallbackAuth.cpp src/dialogs/FallbackAuth.cpp
src/dialogs/ImageOverlay.cpp src/dialogs/ImageOverlay.cpp
src/dialogs/JoinRoom.cpp
src/dialogs/LeaveRoom.cpp
src/dialogs/Logout.cpp src/dialogs/Logout.cpp
src/dialogs/PreviewUploadOverlay.cpp src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp src/dialogs/ReCaptcha.cpp
@ -311,6 +309,8 @@ set(SRC_FILES
src/ui/InfoMessage.cpp src/ui/InfoMessage.cpp
src/ui/Label.cpp src/ui/Label.cpp
src/ui/LoadingIndicator.cpp src/ui/LoadingIndicator.cpp
src/ui/MxcAnimatedImage.cpp
src/ui/MxcMediaProxy.cpp
src/ui/NhekoCursorShape.cpp src/ui/NhekoCursorShape.cpp
src/ui/NhekoDropArea.cpp src/ui/NhekoDropArea.cpp
src/ui/NhekoGlobalObject.cpp src/ui/NhekoGlobalObject.cpp
@ -326,31 +326,38 @@ set(SRC_FILES
src/ui/Theme.cpp src/ui/Theme.cpp
src/ui/ThemeManager.cpp src/ui/ThemeManager.cpp
src/ui/ToggleButton.cpp src/ui/ToggleButton.cpp
src/ui/UIA.cpp
src/ui/UserProfile.cpp src/ui/UserProfile.cpp
src/voip/CallDevices.cpp
src/voip/CallManager.cpp
src/voip/WebRTCSession.cpp
src/encryption/DeviceVerificationFlow.cpp
src/encryption/Olm.cpp
src/encryption/SelfVerificationStatus.cpp
src/encryption/VerificationManager.cpp
# Generic notification stuff # Generic notification stuff
src/notifications/Manager.cpp src/notifications/Manager.cpp
src/AvatarProvider.cpp src/AvatarProvider.cpp
src/BlurhashProvider.cpp src/BlurhashProvider.cpp
src/Cache.cpp src/Cache.cpp
src/CallDevices.cpp
src/CallManager.cpp
src/ChatPage.cpp src/ChatPage.cpp
src/Clipboard.cpp src/Clipboard.cpp
src/ColorImageProvider.cpp src/ColorImageProvider.cpp
src/CompletionProxyModel.cpp src/CompletionProxyModel.cpp
src/DeviceVerificationFlow.cpp
src/EventAccessors.cpp src/EventAccessors.cpp
src/InviteesModel.cpp src/InviteesModel.cpp
src/JdenticonProvider.cpp
src/Logging.cpp src/Logging.cpp
src/LoginPage.cpp src/LoginPage.cpp
src/MainWindow.cpp src/MainWindow.cpp
src/MatrixClient.cpp src/MatrixClient.cpp
src/MemberList.cpp src/MemberList.cpp
src/MxcImageProvider.cpp src/MxcImageProvider.cpp
src/Olm.cpp src/ReadReceiptsModel.cpp
src/ReadReceiptsModel.cpp
src/RegisterPage.cpp src/RegisterPage.cpp
src/SSOHandler.cpp src/SSOHandler.cpp
src/CombinedImagePackModel.cpp src/CombinedImagePackModel.cpp
@ -359,9 +366,9 @@ set(SRC_FILES
src/TrayIcon.cpp src/TrayIcon.cpp
src/UserSettingsPage.cpp src/UserSettingsPage.cpp
src/UsersModel.cpp src/UsersModel.cpp
src/RoomDirectoryModel.cpp
src/RoomsModel.cpp src/RoomsModel.cpp
src/Utils.cpp src/Utils.cpp
src/WebRTCSession.cpp
src/WelcomePage.cpp src/WelcomePage.cpp
src/main.cpp src/main.cpp
@ -381,7 +388,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG deb51ef1d6df870098069312f0a1999550e1eb85 GIT_TAG 7fe7a70fcf7540beb6d7b4847e53a425de66c6bf
) )
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@ -492,8 +499,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/dialogs/CreateRoom.h src/dialogs/CreateRoom.h
src/dialogs/FallbackAuth.h src/dialogs/FallbackAuth.h
src/dialogs/ImageOverlay.h src/dialogs/ImageOverlay.h
src/dialogs/JoinRoom.h
src/dialogs/LeaveRoom.h
src/dialogs/Logout.h src/dialogs/Logout.h
src/dialogs/PreviewUploadOverlay.h src/dialogs/PreviewUploadOverlay.h
src/dialogs/ReCaptcha.h src/dialogs/ReCaptcha.h
@ -520,6 +525,8 @@ qt5_wrap_cpp(MOC_HEADERS
src/ui/InfoMessage.h src/ui/InfoMessage.h
src/ui/Label.h src/ui/Label.h
src/ui/LoadingIndicator.h src/ui/LoadingIndicator.h
src/ui/MxcAnimatedImage.h
src/ui/MxcMediaProxy.h
src/ui/Menu.h src/ui/Menu.h
src/ui/NhekoCursorShape.h src/ui/NhekoCursorShape.h
src/ui/NhekoDropArea.h src/ui/NhekoDropArea.h
@ -535,28 +542,35 @@ qt5_wrap_cpp(MOC_HEADERS
src/ui/Theme.h src/ui/Theme.h
src/ui/ThemeManager.h src/ui/ThemeManager.h
src/ui/ToggleButton.h src/ui/ToggleButton.h
src/ui/UIA.h
src/ui/UserProfile.h src/ui/UserProfile.h
src/voip/CallDevices.h
src/voip/CallManager.h
src/voip/WebRTCSession.h
src/encryption/DeviceVerificationFlow.h
src/encryption/Olm.h
src/encryption/SelfVerificationStatus.h
src/encryption/VerificationManager.h
src/notifications/Manager.h src/notifications/Manager.h
src/AvatarProvider.h src/AvatarProvider.h
src/BlurhashProvider.h src/BlurhashProvider.h
src/CacheCryptoStructs.h src/CacheCryptoStructs.h
src/Cache_p.h src/Cache_p.h
src/CallDevices.h
src/CallManager.h
src/ChatPage.h src/ChatPage.h
src/Clipboard.h src/Clipboard.h
src/CombinedImagePackModel.h src/CombinedImagePackModel.h
src/CompletionProxyModel.h src/CompletionProxyModel.h
src/DeviceVerificationFlow.h
src/ImagePackListModel.h src/ImagePackListModel.h
src/InviteesModel.h src/InviteesModel.h
src/JdenticonProvider.h
src/LoginPage.h src/LoginPage.h
src/MainWindow.h src/MainWindow.h
src/MemberList.h src/MemberList.h
src/MxcImageProvider.h src/MxcImageProvider.h
src/Olm.h
src/RegisterPage.h src/RegisterPage.h
src/RoomsModel.h src/RoomsModel.h
src/SSOHandler.h src/SSOHandler.h
@ -564,7 +578,8 @@ qt5_wrap_cpp(MOC_HEADERS
src/TrayIcon.h src/TrayIcon.h
src/UserSettingsPage.h src/UserSettingsPage.h
src/UsersModel.h src/UsersModel.h
src/WebRTCSession.h src/RoomDirectoryModel.h
src/RoomsModel.h
src/WelcomePage.h src/WelcomePage.h
src/ReadReceiptsModel.h src/ReadReceiptsModel.h
) )
@ -576,7 +591,7 @@ include(Translations)
set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC}) set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
if (APPLE) if (APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa -framework UserNotifications")
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/notifications/ManagerMac.cpp src/emoji/MacHelper.mm)
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0") 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) set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)

View File

@ -77,6 +77,7 @@ sudo dnf install nheko
#### Gentoo Linux #### Gentoo Linux
```bash ```bash
sudo eselect repository enable guru sudo eselect repository enable guru
sudo emaint sync -r guru
sudo emerge -a nheko sudo emerge -a nheko
``` ```
@ -126,11 +127,31 @@ choco install nheko-reborn
### FAQ ### FAQ
## ---
**Q:** Why don't videos run for me on Windows? **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). **A:** You're probably missing the required video codecs, download [K-Lite Codec Pack](https://codecguide.com/download_kl.htm).
##
---
**Q:** What commands are supported by nheko?
**A:** See <https://github.com/Nheko-Reborn/nheko/wiki/Commands>
---
**Q:** Does nheko support end-to-end encryption (EE2E)?
**A:** Yes, see [feature list](#features)
---
**Q:** Can I test a bleeding edge development version?
**A:** Checkout nightly builds <https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/>
---
### Build Requirements ### Build Requirements
@ -150,7 +171,7 @@ choco install nheko-reborn
- Voice call support: dtls, opus, rtpmanager, srtp, webrtc - Voice call support: dtls, opus, rtpmanager, srtp, webrtc
- Video call support (optional): compositor, opengl, qmlgl, rtp, vpx - Video call support (optional): compositor, opengl, qmlgl, rtp, vpx
- [libnice](https://gitlab.freedesktop.org/libnice/libnice) - [libnice](https://gitlab.freedesktop.org/libnice/libnice)
- [qtkeychain](https://github.com/frankosterfeld/qtkeychain) - [qtkeychain](https://github.com/frankosterfeld/qtkeychain) (You need at least version 0.12 for proper Gnome Keychain support)
- A compiler that supports C++ 17: - A compiler that supports C++ 17:
- Clang 6 (tested on Travis CI) - Clang 6 (tested on Travis CI)
- GCC 7 (tested on Travis CI) - GCC 7 (tested on Travis CI)

View File

@ -163,7 +163,7 @@ modules:
buildsystem: cmake-ninja buildsystem: cmake-ninja
name: mtxclient name: mtxclient
sources: sources:
- commit: deb51ef1d6df870098069312f0a1999550e1eb85 - commit: 7fe7a70fcf7540beb6d7b4847e53a425de66c6bf
type: git type: git
url: https://github.com/Nheko-Reborn/mtxclient.git url: https://github.com/Nheko-Reborn/mtxclient.git
- config-opts: - config-opts:

View File

@ -1,11 +1,11 @@
# emoji-test.txt # emoji-test.txt
# Date: 2020-09-12, 22:19:50 GMT # Date: 2021-08-26, 17:22:23 GMT
# © 2020 Unicode®, Inc. # © 2021 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. # 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 # For terms of use, see http://www.unicode.org/terms_of_use.html
# #
# Emoji Keyboard/Display Test Data for UTS #51 # Emoji Keyboard/Display Test Data for UTS #51
# Version: 13.1 # Version: 14.0
# #
# For documentation and usage, see http://www.unicode.org/reports/tr51 # For documentation and usage, see http://www.unicode.org/reports/tr51
# #
@ -43,6 +43,7 @@
1F602 ; fully-qualified # 😂 E0.6 face with tears of joy 1F602 ; fully-qualified # 😂 E0.6 face with tears of joy
1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face 1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face
1F643 ; fully-qualified # 🙃 E1.0 upside-down face 1F643 ; fully-qualified # 🙃 E1.0 upside-down face
1FAE0 ; fully-qualified # 🫠 E14.0 melting face
1F609 ; fully-qualified # 😉 E0.6 winking face 1F609 ; fully-qualified # 😉 E0.6 winking face
1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes 1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes
1F607 ; fully-qualified # 😇 E1.0 smiling face with halo 1F607 ; fully-qualified # 😇 E1.0 smiling face with halo
@ -68,10 +69,13 @@
1F911 ; fully-qualified # 🤑 E1.0 money-mouth face 1F911 ; fully-qualified # 🤑 E1.0 money-mouth face
# subgroup: face-hand # subgroup: face-hand
1F917 ; fully-qualified # 🤗 E1.0 hugging face 1F917 ; fully-qualified # 🤗 E1.0 smiling face with open hands
1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth 1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth
1FAE2 ; fully-qualified # 🫢 E14.0 face with open eyes and hand over mouth
1FAE3 ; fully-qualified # 🫣 E14.0 face with peeking eye
1F92B ; fully-qualified # 🤫 E5.0 shushing face 1F92B ; fully-qualified # 🤫 E5.0 shushing face
1F914 ; fully-qualified # 🤔 E1.0 thinking face 1F914 ; fully-qualified # 🤔 E1.0 thinking face
1FAE1 ; fully-qualified # 🫡 E14.0 saluting face
# subgroup: face-neutral-skeptical # subgroup: face-neutral-skeptical
1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face 1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face
@ -79,6 +83,7 @@
1F610 ; fully-qualified # 😐 E0.7 neutral face 1F610 ; fully-qualified # 😐 E0.7 neutral face
1F611 ; fully-qualified # 😑 E1.0 expressionless face 1F611 ; fully-qualified # 😑 E1.0 expressionless face
1F636 ; fully-qualified # 😶 E1.0 face without mouth 1F636 ; fully-qualified # 😶 E1.0 face without mouth
1FAE5 ; fully-qualified # 🫥 E14.0 dotted line face
1F636 200D 1F32B FE0F ; fully-qualified # 😶‍🌫️ E13.1 face in clouds 1F636 200D 1F32B FE0F ; fully-qualified # 😶‍🌫️ E13.1 face in clouds
1F636 200D 1F32B ; minimally-qualified # 😶‍🌫 E13.1 face in clouds 1F636 200D 1F32B ; minimally-qualified # 😶‍🌫 E13.1 face in clouds
1F60F ; fully-qualified # 😏 E0.6 smirking face 1F60F ; fully-qualified # 😏 E0.6 smirking face
@ -105,7 +110,7 @@
1F975 ; fully-qualified # 🥵 E11.0 hot face 1F975 ; fully-qualified # 🥵 E11.0 hot face
1F976 ; fully-qualified # 🥶 E11.0 cold face 1F976 ; fully-qualified # 🥶 E11.0 cold face
1F974 ; fully-qualified # 🥴 E11.0 woozy face 1F974 ; fully-qualified # 🥴 E11.0 woozy face
1F635 ; fully-qualified # 😵 E0.6 knocked-out face 1F635 ; fully-qualified # 😵 E0.6 face with crossed-out eyes
1F635 200D 1F4AB ; fully-qualified # 😵‍💫 E13.1 face with spiral eyes 1F635 200D 1F4AB ; fully-qualified # 😵‍💫 E13.1 face with spiral eyes
1F92F ; fully-qualified # 🤯 E5.0 exploding head 1F92F ; fully-qualified # 🤯 E5.0 exploding head
@ -121,6 +126,7 @@
# subgroup: face-concerned # subgroup: face-concerned
1F615 ; fully-qualified # 😕 E1.0 confused face 1F615 ; fully-qualified # 😕 E1.0 confused face
1FAE4 ; fully-qualified # 🫤 E14.0 face with diagonal mouth
1F61F ; fully-qualified # 😟 E1.0 worried face 1F61F ; fully-qualified # 😟 E1.0 worried face
1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face 1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face
2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face 2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face
@ -130,6 +136,7 @@
1F632 ; fully-qualified # 😲 E0.6 astonished face 1F632 ; fully-qualified # 😲 E0.6 astonished face
1F633 ; fully-qualified # 😳 E0.6 flushed face 1F633 ; fully-qualified # 😳 E0.6 flushed face
1F97A ; fully-qualified # 🥺 E11.0 pleading face 1F97A ; fully-qualified # 🥺 E11.0 pleading face
1F979 ; fully-qualified # 🥹 E14.0 face holding back tears
1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth 1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth
1F627 ; fully-qualified # 😧 E1.0 anguished face 1F627 ; fully-qualified # 😧 E1.0 anguished face
1F628 ; fully-qualified # 😨 E0.6 fearful face 1F628 ; fully-qualified # 😨 E0.6 fearful face
@ -232,8 +239,8 @@
1F4AD ; fully-qualified # 💭 E1.0 thought balloon 1F4AD ; fully-qualified # 💭 E1.0 thought balloon
1F4A4 ; fully-qualified # 💤 E0.6 zzz 1F4A4 ; fully-qualified # 💤 E0.6 zzz
# Smileys & Emotion subtotal: 170 # Smileys & Emotion subtotal: 177
# Smileys & Emotion subtotal: 170 w/o modifiers # Smileys & Emotion subtotal: 177 w/o modifiers
# group: People & Body # group: People & Body
@ -269,6 +276,30 @@
1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone 1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone
1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone 1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone 1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone
1FAF1 ; fully-qualified # 🫱 E14.0 rightwards hand
1FAF1 1F3FB ; fully-qualified # 🫱🏻 E14.0 rightwards hand: light skin tone
1FAF1 1F3FC ; fully-qualified # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
1FAF1 1F3FD ; fully-qualified # 🫱🏽 E14.0 rightwards hand: medium skin tone
1FAF1 1F3FE ; fully-qualified # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
1FAF1 1F3FF ; fully-qualified # 🫱🏿 E14.0 rightwards hand: dark skin tone
1FAF2 ; fully-qualified # 🫲 E14.0 leftwards hand
1FAF2 1F3FB ; fully-qualified # 🫲🏻 E14.0 leftwards hand: light skin tone
1FAF2 1F3FC ; fully-qualified # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
1FAF2 1F3FD ; fully-qualified # 🫲🏽 E14.0 leftwards hand: medium skin tone
1FAF2 1F3FE ; fully-qualified # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
1FAF2 1F3FF ; fully-qualified # 🫲🏿 E14.0 leftwards hand: dark skin tone
1FAF3 ; fully-qualified # 🫳 E14.0 palm down hand
1FAF3 1F3FB ; fully-qualified # 🫳🏻 E14.0 palm down hand: light skin tone
1FAF3 1F3FC ; fully-qualified # 🫳🏼 E14.0 palm down hand: medium-light skin tone
1FAF3 1F3FD ; fully-qualified # 🫳🏽 E14.0 palm down hand: medium skin tone
1FAF3 1F3FE ; fully-qualified # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
1FAF3 1F3FF ; fully-qualified # 🫳🏿 E14.0 palm down hand: dark skin tone
1FAF4 ; fully-qualified # 🫴 E14.0 palm up hand
1FAF4 1F3FB ; fully-qualified # 🫴🏻 E14.0 palm up hand: light skin tone
1FAF4 1F3FC ; fully-qualified # 🫴🏼 E14.0 palm up hand: medium-light skin tone
1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone
1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone
# subgroup: hand-fingers-partial # subgroup: hand-fingers-partial
1F44C ; fully-qualified # 👌 E0.6 OK hand 1F44C ; fully-qualified # 👌 E0.6 OK hand
@ -302,6 +333,12 @@
1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone 1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone
1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone 1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone 1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone
1FAF0 ; fully-qualified # 🫰 E14.0 hand with index finger and thumb crossed
1FAF0 1F3FB ; fully-qualified # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
1FAF0 1F3FC ; fully-qualified # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
1FAF0 1F3FD ; fully-qualified # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
1FAF0 1F3FE ; fully-qualified # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
1FAF0 1F3FF ; fully-qualified # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
1F91F ; fully-qualified # 🤟 E5.0 love-you gesture 1F91F ; fully-qualified # 🤟 E5.0 love-you gesture
1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone 1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone
1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone 1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
@ -359,6 +396,12 @@
261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone 261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone
261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone 261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone
261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone 261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone
1FAF5 ; fully-qualified # 🫵 E14.0 index pointing at the viewer
1FAF5 1F3FB ; fully-qualified # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
1FAF5 1F3FC ; fully-qualified # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
1FAF5 1F3FD ; fully-qualified # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
1FAF5 1F3FE ; fully-qualified # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
1FAF5 1F3FF ; fully-qualified # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
# subgroup: hand-fingers-closed # subgroup: hand-fingers-closed
1F44D ; fully-qualified # 👍 E0.6 thumbs up 1F44D ; fully-qualified # 👍 E0.6 thumbs up
@ -411,6 +454,12 @@
1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone 1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone
1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone 1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone
1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone 1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone
1FAF6 ; fully-qualified # 🫶 E14.0 heart hands
1FAF6 1F3FB ; fully-qualified # 🫶🏻 E14.0 heart hands: light skin tone
1FAF6 1F3FC ; fully-qualified # 🫶🏼 E14.0 heart hands: medium-light skin tone
1FAF6 1F3FD ; fully-qualified # 🫶🏽 E14.0 heart hands: medium skin tone
1FAF6 1F3FE ; fully-qualified # 🫶🏾 E14.0 heart hands: medium-dark skin tone
1FAF6 1F3FF ; fully-qualified # 🫶🏿 E14.0 heart hands: dark skin tone
1F450 ; fully-qualified # 👐 E0.6 open hands 1F450 ; fully-qualified # 👐 E0.6 open hands
1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone 1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone
1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone 1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone
@ -424,6 +473,31 @@
1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone 1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone 1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
1F91D ; fully-qualified # 🤝 E3.0 handshake 1F91D ; fully-qualified # 🤝 E3.0 handshake
1F91D 1F3FB ; fully-qualified # 🤝🏻 E3.0 handshake: light skin tone
1F91D 1F3FC ; fully-qualified # 🤝🏼 E3.0 handshake: medium-light skin tone
1F91D 1F3FD ; fully-qualified # 🤝🏽 E3.0 handshake: medium skin tone
1F91D 1F3FE ; fully-qualified # 🤝🏾 E3.0 handshake: medium-dark skin tone
1F91D 1F3FF ; fully-qualified # 🤝🏿 E3.0 handshake: dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏻‍🫲🏿 E14.0 handshake: light skin tone, dark skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏼‍🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏼‍🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏼‍🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏼‍🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏽‍🫲🏻 E14.0 handshake: medium skin tone, light skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏽‍🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏽‍🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏽‍🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏾‍🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏾‍🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏾‍🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏾‍🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏿‍🫲🏻 E14.0 handshake: dark skin tone, light skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏿‍🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏿‍🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏿‍🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
1F64F ; fully-qualified # 🙏 E0.6 folded hands 1F64F ; fully-qualified # 🙏 E0.6 folded hands
1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone 1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone
1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone 1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone
@ -501,6 +575,7 @@
1F441 ; unqualified # 👁 E0.7 eye 1F441 ; unqualified # 👁 E0.7 eye
1F445 ; fully-qualified # 👅 E0.6 tongue 1F445 ; fully-qualified # 👅 E0.6 tongue
1F444 ; fully-qualified # 👄 E0.6 mouth 1F444 ; fully-qualified # 👄 E0.6 mouth
1FAE6 ; fully-qualified # 🫦 E14.0 biting lip
# subgroup: person # subgroup: person
1F476 ; fully-qualified # 👶 E0.6 baby 1F476 ; fully-qualified # 👶 E0.6 baby
@ -1472,6 +1547,12 @@
1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone 1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone
1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone 1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone
1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone 1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone
1FAC5 ; fully-qualified # 🫅 E14.0 person with crown
1FAC5 1F3FB ; fully-qualified # 🫅🏻 E14.0 person with crown: light skin tone
1FAC5 1F3FC ; fully-qualified # 🫅🏼 E14.0 person with crown: medium-light skin tone
1FAC5 1F3FD ; fully-qualified # 🫅🏽 E14.0 person with crown: medium skin tone
1FAC5 1F3FE ; fully-qualified # 🫅🏾 E14.0 person with crown: medium-dark skin tone
1FAC5 1F3FF ; fully-qualified # 🫅🏿 E14.0 person with crown: dark skin tone
1F934 ; fully-qualified # 🤴 E3.0 prince 1F934 ; fully-qualified # 🤴 E3.0 prince
1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone 1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone
1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone 1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone
@ -1592,6 +1673,18 @@
1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone 1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone
1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone 1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone 1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone
1FAC3 ; fully-qualified # 🫃 E14.0 pregnant man
1FAC3 1F3FB ; fully-qualified # 🫃🏻 E14.0 pregnant man: light skin tone
1FAC3 1F3FC ; fully-qualified # 🫃🏼 E14.0 pregnant man: medium-light skin tone
1FAC3 1F3FD ; fully-qualified # 🫃🏽 E14.0 pregnant man: medium skin tone
1FAC3 1F3FE ; fully-qualified # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
1FAC3 1F3FF ; fully-qualified # 🫃🏿 E14.0 pregnant man: dark skin tone
1FAC4 ; fully-qualified # 🫄 E14.0 pregnant person
1FAC4 1F3FB ; fully-qualified # 🫄🏻 E14.0 pregnant person: light skin tone
1FAC4 1F3FC ; fully-qualified # 🫄🏼 E14.0 pregnant person: medium-light skin tone
1FAC4 1F3FD ; fully-qualified # 🫄🏽 E14.0 pregnant person: medium skin tone
1FAC4 1F3FE ; fully-qualified # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
1FAC4 1F3FF ; fully-qualified # 🫄🏿 E14.0 pregnant person: dark skin tone
1F931 ; fully-qualified # 🤱 E5.0 breast-feeding 1F931 ; fully-qualified # 🤱 E5.0 breast-feeding
1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone 1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone
1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone 1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
@ -1862,6 +1955,7 @@
1F9DF 200D 2642 ; minimally-qualified # 🧟‍♂ E5.0 man zombie 1F9DF 200D 2642 ; minimally-qualified # 🧟‍♂ E5.0 man zombie
1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ E5.0 woman zombie 1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ E5.0 woman zombie
1F9DF 200D 2640 ; minimally-qualified # 🧟‍♀ E5.0 woman zombie 1F9DF 200D 2640 ; minimally-qualified # 🧟‍♀ E5.0 woman zombie
1F9CC ; fully-qualified # 🧌 E14.0 troll
# subgroup: person-activity # subgroup: person-activity
1F486 ; fully-qualified # 💆 E0.6 person getting massage 1F486 ; fully-qualified # 💆 E0.6 person getting massage
@ -3168,8 +3262,8 @@
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging 1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
1F463 ; fully-qualified # 👣 E0.6 footprints 1F463 ; fully-qualified # 👣 E0.6 footprints
# People & Body subtotal: 2899 # People & Body subtotal: 2986
# People & Body subtotal: 494 w/o modifiers # People & Body subtotal: 506 w/o modifiers
# group: Component # group: Component
@ -3304,6 +3398,7 @@
1F988 ; fully-qualified # 🦈 E3.0 shark 1F988 ; fully-qualified # 🦈 E3.0 shark
1F419 ; fully-qualified # 🐙 E0.6 octopus 1F419 ; fully-qualified # 🐙 E0.6 octopus
1F41A ; fully-qualified # 🐚 E0.6 spiral shell 1F41A ; fully-qualified # 🐚 E0.6 spiral shell
1FAB8 ; fully-qualified # 🪸 E14.0 coral
# subgroup: animal-bug # subgroup: animal-bug
1F40C ; fully-qualified # 🐌 E0.6 snail 1F40C ; fully-qualified # 🐌 E0.6 snail
@ -3329,6 +3424,7 @@
1F490 ; fully-qualified # 💐 E0.6 bouquet 1F490 ; fully-qualified # 💐 E0.6 bouquet
1F338 ; fully-qualified # 🌸 E0.6 cherry blossom 1F338 ; fully-qualified # 🌸 E0.6 cherry blossom
1F4AE ; fully-qualified # 💮 E0.6 white flower 1F4AE ; fully-qualified # 💮 E0.6 white flower
1FAB7 ; fully-qualified # 🪷 E14.0 lotus
1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette 1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette
1F3F5 ; unqualified # 🏵 E0.7 rosette 1F3F5 ; unqualified # 🏵 E0.7 rosette
1F339 ; fully-qualified # 🌹 E0.6 rose 1F339 ; fully-qualified # 🌹 E0.6 rose
@ -3353,9 +3449,11 @@
1F341 ; fully-qualified # 🍁 E0.6 maple leaf 1F341 ; fully-qualified # 🍁 E0.6 maple leaf
1F342 ; fully-qualified # 🍂 E0.6 fallen leaf 1F342 ; fully-qualified # 🍂 E0.6 fallen leaf
1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind 1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
1FAB9 ; fully-qualified # 🪹 E14.0 empty nest
1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
# Animals & Nature subtotal: 147 # Animals & Nature subtotal: 151
# Animals & Nature subtotal: 147 w/o modifiers # Animals & Nature subtotal: 151 w/o modifiers
# group: Food & Drink # group: Food & Drink
@ -3396,6 +3494,7 @@
1F9C5 ; fully-qualified # 🧅 E12.0 onion 1F9C5 ; fully-qualified # 🧅 E12.0 onion
1F344 ; fully-qualified # 🍄 E0.6 mushroom 1F344 ; fully-qualified # 🍄 E0.6 mushroom
1F95C ; fully-qualified # 🥜 E3.0 peanuts 1F95C ; fully-qualified # 🥜 E3.0 peanuts
1FAD8 ; fully-qualified # 🫘 E14.0 beans
1F330 ; fully-qualified # 🌰 E0.6 chestnut 1F330 ; fully-qualified # 🌰 E0.6 chestnut
# subgroup: food-prepared # subgroup: food-prepared
@ -3491,6 +3590,7 @@
1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs 1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs
1F942 ; fully-qualified # 🥂 E3.0 clinking glasses 1F942 ; fully-qualified # 🥂 E3.0 clinking glasses
1F943 ; fully-qualified # 🥃 E3.0 tumbler glass 1F943 ; fully-qualified # 🥃 E3.0 tumbler glass
1FAD7 ; fully-qualified # 🫗 E14.0 pouring liquid
1F964 ; fully-qualified # 🥤 E5.0 cup with straw 1F964 ; fully-qualified # 🥤 E5.0 cup with straw
1F9CB ; fully-qualified # 🧋 E13.0 bubble tea 1F9CB ; fully-qualified # 🧋 E13.0 bubble tea
1F9C3 ; fully-qualified # 🧃 E12.0 beverage box 1F9C3 ; fully-qualified # 🧃 E12.0 beverage box
@ -3504,10 +3604,11 @@
1F374 ; fully-qualified # 🍴 E0.6 fork and knife 1F374 ; fully-qualified # 🍴 E0.6 fork and knife
1F944 ; fully-qualified # 🥄 E3.0 spoon 1F944 ; fully-qualified # 🥄 E3.0 spoon
1F52A ; fully-qualified # 🔪 E0.6 kitchen knife 1F52A ; fully-qualified # 🔪 E0.6 kitchen knife
1FAD9 ; fully-qualified # 🫙 E14.0 jar
1F3FA ; fully-qualified # 🏺 E1.0 amphora 1F3FA ; fully-qualified # 🏺 E1.0 amphora
# Food & Drink subtotal: 131 # Food & Drink subtotal: 134
# Food & Drink subtotal: 131 w/o modifiers # Food & Drink subtotal: 134 w/o modifiers
# group: Travel & Places # group: Travel & Places
@ -3597,6 +3698,7 @@
2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs 2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs
2668 ; unqualified # ♨ E0.6 hot springs 2668 ; unqualified # ♨ E0.6 hot springs
1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse 1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse
1F6DD ; fully-qualified # 🛝 E14.0 playground slide
1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel 1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel
1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster 1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster
1F488 ; fully-qualified # 💈 E0.6 barber pole 1F488 ; fully-qualified # 💈 E0.6 barber pole
@ -3652,6 +3754,7 @@
1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum 1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum
1F6E2 ; unqualified # 🛢 E0.7 oil drum 1F6E2 ; unqualified # 🛢 E0.7 oil drum
26FD ; fully-qualified # ⛽ E0.6 fuel pump 26FD ; fully-qualified # ⛽ E0.6 fuel pump
1F6DE ; fully-qualified # 🛞 E14.0 wheel
1F6A8 ; fully-qualified # 🚨 E0.6 police car light 1F6A8 ; fully-qualified # 🚨 E0.6 police car light
1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light 1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light
1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light 1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light
@ -3660,6 +3763,7 @@
# subgroup: transport-water # subgroup: transport-water
2693 ; fully-qualified # ⚓ E0.6 anchor 2693 ; fully-qualified # ⚓ E0.6 anchor
1F6DF ; fully-qualified # 🛟 E14.0 ring buoy
26F5 ; fully-qualified # ⛵ E0.6 sailboat 26F5 ; fully-qualified # ⛵ E0.6 sailboat
1F6F6 ; fully-qualified # 🛶 E3.0 canoe 1F6F6 ; fully-qualified # 🛶 E3.0 canoe
1F6A4 ; fully-qualified # 🚤 E0.6 speedboat 1F6A4 ; fully-qualified # 🚤 E0.6 speedboat
@ -3797,8 +3901,8 @@
1F4A7 ; fully-qualified # 💧 E0.6 droplet 1F4A7 ; fully-qualified # 💧 E0.6 droplet
1F30A ; fully-qualified # 🌊 E0.6 water wave 1F30A ; fully-qualified # 🌊 E0.6 water wave
# Travel & Places subtotal: 264 # Travel & Places subtotal: 267
# Travel & Places subtotal: 264 w/o modifiers # Travel & Places subtotal: 267 w/o modifiers
# group: Activities # group: Activities
@ -3874,6 +3978,7 @@
1F52E ; fully-qualified # 🔮 E0.6 crystal ball 1F52E ; fully-qualified # 🔮 E0.6 crystal ball
1FA84 ; fully-qualified # 🪄 E13.0 magic wand 1FA84 ; fully-qualified # 🪄 E13.0 magic wand
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet 1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F3AE ; fully-qualified # 🎮 E0.6 video game 1F3AE ; fully-qualified # 🎮 E0.6 video game
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick 1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
1F579 ; unqualified # 🕹 E0.7 joystick 1F579 ; unqualified # 🕹 E0.7 joystick
@ -3882,6 +3987,7 @@
1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece 1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece
1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear 1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear
1FA85 ; fully-qualified # 🪅 E13.0 piñata 1FA85 ; fully-qualified # 🪅 E13.0 piñata
1FAA9 ; fully-qualified # 🪩 E14.0 mirror ball
1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls 1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls
2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit 2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit
2660 ; unqualified # ♠ E0.6 spade suit 2660 ; unqualified # ♠ E0.6 spade suit
@ -3907,8 +4013,8 @@
1F9F6 ; fully-qualified # 🧶 E11.0 yarn 1F9F6 ; fully-qualified # 🧶 E11.0 yarn
1FAA2 ; fully-qualified # 🪢 E13.0 knot 1FAA2 ; fully-qualified # 🪢 E13.0 knot
# Activities subtotal: 95 # Activities subtotal: 97
# Activities subtotal: 95 w/o modifiers # Activities subtotal: 97 w/o modifiers
# group: Objects # group: Objects
@ -4009,6 +4115,7 @@
# subgroup: computer # subgroup: computer
1F50B ; fully-qualified # 🔋 E0.6 battery 1F50B ; fully-qualified # 🔋 E0.6 battery
1FAAB ; fully-qualified # 🪫 E14.0 low battery
1F50C ; fully-qualified # 🔌 E0.6 electric plug 1F50C ; fully-qualified # 🔌 E0.6 electric plug
1F4BB ; fully-qualified # 💻 E0.6 laptop 1F4BB ; fully-qualified # 💻 E0.6 laptop
1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer 1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer
@ -4207,7 +4314,9 @@
1FA78 ; fully-qualified # 🩸 E12.0 drop of blood 1FA78 ; fully-qualified # 🩸 E12.0 drop of blood
1F48A ; fully-qualified # 💊 E0.6 pill 1F48A ; fully-qualified # 💊 E0.6 pill
1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage 1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage
1FA7C ; fully-qualified # 🩼 E14.0 crutch
1FA7A ; fully-qualified # 🩺 E12.0 stethoscope 1FA7A ; fully-qualified # 🩺 E12.0 stethoscope
1FA7B ; fully-qualified # 🩻 E14.0 x-ray
# subgroup: household # subgroup: household
1F6AA ; fully-qualified # 🚪 E0.6 door 1F6AA ; fully-qualified # 🚪 E0.6 door
@ -4232,6 +4341,7 @@
1F9FB ; fully-qualified # 🧻 E11.0 roll of paper 1F9FB ; fully-qualified # 🧻 E11.0 roll of paper
1FAA3 ; fully-qualified # 🪣 E13.0 bucket 1FAA3 ; fully-qualified # 🪣 E13.0 bucket
1F9FC ; fully-qualified # 🧼 E11.0 soap 1F9FC ; fully-qualified # 🧼 E11.0 soap
1FAE7 ; fully-qualified # 🫧 E14.0 bubbles
1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush 1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush
1F9FD ; fully-qualified # 🧽 E11.0 sponge 1F9FD ; fully-qualified # 🧽 E11.0 sponge
1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher 1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher
@ -4246,9 +4356,10 @@
26B1 ; unqualified # ⚱ E1.0 funeral urn 26B1 ; unqualified # ⚱ E1.0 funeral urn
1F5FF ; fully-qualified # 🗿 E0.6 moai 1F5FF ; fully-qualified # 🗿 E0.6 moai
1FAA7 ; fully-qualified # 🪧 E13.0 placard 1FAA7 ; fully-qualified # 🪧 E13.0 placard
1FAAA ; fully-qualified # 🪪 E14.0 identification card
# Objects subtotal: 299 # Objects subtotal: 304
# Objects subtotal: 299 w/o modifiers # Objects subtotal: 304 w/o modifiers
# group: Symbols # group: Symbols
@ -4409,6 +4520,7 @@
2795 ; fully-qualified # E0.6 plus 2795 ; fully-qualified # E0.6 plus
2796 ; fully-qualified # E0.6 minus 2796 ; fully-qualified # E0.6 minus
2797 ; fully-qualified # ➗ E0.6 divide 2797 ; fully-qualified # ➗ E0.6 divide
1F7F0 ; fully-qualified # 🟰 E14.0 heavy equals sign
267E FE0F ; fully-qualified # ♾️ E11.0 infinity 267E FE0F ; fully-qualified # ♾️ E11.0 infinity
267E ; unqualified # ♾ E11.0 infinity 267E ; unqualified # ♾ E11.0 infinity
@ -4581,8 +4693,8 @@
1F533 ; fully-qualified # 🔳 E0.6 white square button 1F533 ; fully-qualified # 🔳 E0.6 white square button
1F532 ; fully-qualified # 🔲 E0.6 black square button 1F532 ; fully-qualified # 🔲 E0.6 black square button
# Symbols subtotal: 301 # Symbols subtotal: 302
# Symbols subtotal: 301 w/o modifiers # Symbols subtotal: 302 w/o modifiers
# group: Flags # group: Flags
@ -4871,7 +4983,7 @@
# Flags subtotal: 275 w/o modifiers # Flags subtotal: 275 w/o modifiers
# Status Counts # Status Counts
# fully-qualified : 3512 # fully-qualified : 3624
# minimally-qualified : 817 # minimally-qualified : 817
# unqualified : 252 # unqualified : 252
# component : 9 # component : 9

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="-10 0 1792 1792"
id="svg866"
width="1792"
height="1792"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs870" />
<path
fill="currentColor"
d="m 1629,1056 q 0,5 -1,7 -64,268 -268,434.5 Q 1156,1664 882,1664 736,1664 599.5,1609 463,1554 356,1452 l -129,129 q -19,19 -45,19 -26,0 -45,-19 -19,-19 -19,-45 v -448 q 0,-26 19,-45 19,-19 45,-19 h 448 q 26,0 45,19 19,19 19,45 0,26 -19,45 l -137,137 q 71,66 161,102 90,36 187,36 134,0 250,-65 116,-65 186,-179 11,-17 53,-117 8,-23 30,-23 h 192 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 25,-800 v 448 q 0,26 -19,45 -19,19 -45,19 h -448 q -26,0 -45,-19 -19,-19 -19,-45 0,-26 19,-45 L 1235,521 Q 1087,384 886,384 q -134,0 -250,65 -116,65 -186,179 -11,17 -53,117 -8,23 -30,23 H 168 q -13,0 -22.5,-9.5 Q 136,749 136,736 v -7 Q 201,461 406,294.5 611,128 886,128 q 146,0 284,55.5 138,55.5 245,156.5 l 130,-129 q 19,-19 45,-19 26,0 45,19 19,19 19,45 z"
id="path864" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

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

3325
resources/langs/nheko_id.ts Normal file

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

View File

@ -3,7 +3,7 @@
<component type="desktop"> <component type="desktop">
<id>nheko.desktop</id> <id>nheko.desktop</id>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later and CC-BY</project_license> <project_license>GPL-3.0-or-later</project_license>
<name>nheko</name> <name>nheko</name>
<summary>Desktop client for the Matrix protocol</summary> <summary>Desktop client for the Matrix protocol</summary>
<description> <description>

View File

@ -12,6 +12,7 @@ Rectangle {
property string url property string url
property string userid property string userid
property string roomid
property string displayName property string displayName
property alias textColor: label.color property alias textColor: label.color
property bool crop: true property bool crop: true
@ -35,10 +36,29 @@ Rectangle {
font.pixelSize: avatar.height / 2 font.pixelSize: avatar.height / 2
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
visible: img.status != Image.Ready visible: img.status != Image.Ready && !Settings.useIdenticon
color: Nheko.colors.text color: Nheko.colors.text
} }
Image {
id: identicon
anchors.fill: parent
visible: Settings.useIdenticon && img.status != Image.Ready
source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : ""
MouseArea {
anchors.fill: parent
Ripple {
rippleTarget: parent
color: Qt.rgba(Nheko.colors.alternateBase.r, Nheko.colors.alternateBase.g, Nheko.colors.alternateBase.b, 0.5)
}
}
}
Image { Image {
id: img id: img
@ -49,7 +69,7 @@ Rectangle {
smooth: true smooth: true
sourceSize.width: avatar.width sourceSize.width: avatar.width
sourceSize.height: avatar.height sourceSize.height: avatar.height
source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100.0 : 25.0) + ((avatar.crop) ? "" : "&scale")) : "" source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale")) : ""
MouseArea { MouseArea {
id: mouseArea id: mouseArea

View File

@ -2,12 +2,15 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9 import QtQuick 2.15
import QtQuick.Controls 2.5 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import "components" import "components"
import im.nheko 1.0 import im.nheko 1.0
// this needs to be last
import QtQml 2.15
Rectangle { Rectangle {
id: chatPage id: chatPage
@ -18,7 +21,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width
pageIndex: Rooms.currentRoom ? 2 : 1 pageIndex: (Rooms.currentRoom || Rooms.currentRoomPreview.roomid) ? 2 : 1
AdaptiveLayoutElement { AdaptiveLayoutElement {
id: communityListC id: communityListC
@ -41,6 +44,7 @@ Rectangle {
value: communityListC.preferredWidth value: communityListC.preferredWidth
when: !adaptiveView.singlePageMode when: !adaptiveView.singlePageMode
delayed: true delayed: true
restoreMode: Binding.RestoreBindingOrValue
} }
} }
@ -66,6 +70,7 @@ Rectangle {
value: roomListC.preferredWidth value: roomListC.preferredWidth
when: !adaptiveView.singlePageMode when: !adaptiveView.singlePageMode
delayed: true delayed: true
restoreMode: Binding.RestoreBindingOrValue
} }
} }

View File

@ -47,29 +47,32 @@ Page {
} }
delegate: Rectangle { delegate: ItemDelegate {
id: communityItem id: communityItem
property color background: Nheko.colors.window property color backgroundColor: Nheko.colors.window
property color importantText: Nheko.colors.text property color importantText: Nheko.colors.text
property color unimportantText: Nheko.colors.buttonText property color unimportantText: Nheko.colors.buttonText
property color bubbleBackground: Nheko.colors.highlight property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText property color bubbleText: Nheko.colors.highlightedText
color: background background: Rectangle {
color: backgroundColor
}
height: avatarSize + 2 * Nheko.paddingMedium height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width width: ListView.view.width
state: "normal" state: "normal"
ToolTip.visible: hovered.hovered && collapsed ToolTip.visible: hovered && collapsed
ToolTip.text: model.tooltip ToolTip.text: model.tooltip
states: [ states: [
State { State {
name: "highlight" name: "highlight"
when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id) when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId == model.id)
PropertyChanges { PropertyChanges {
target: communityItem target: communityItem
background: Nheko.colors.dark backgroundColor: Nheko.colors.dark
importantText: Nheko.colors.brightText importantText: Nheko.colors.brightText
unimportantText: Nheko.colors.brightText unimportantText: Nheko.colors.brightText
bubbleBackground: Nheko.colors.highlight bubbleBackground: Nheko.colors.highlight
@ -83,7 +86,7 @@ Page {
PropertyChanges { PropertyChanges {
target: communityItem target: communityItem
background: Nheko.colors.highlight backgroundColor: Nheko.colors.highlight
importantText: Nheko.colors.highlightedText importantText: Nheko.colors.highlightedText
unimportantText: Nheko.colors.highlightedText unimportantText: Nheko.colors.highlightedText
bubbleBackground: Nheko.colors.highlightedText bubbleBackground: Nheko.colors.highlightedText
@ -93,24 +96,20 @@ Page {
} }
] ]
TapHandler { Item {
margin: -Nheko.paddingSmall anchors.fill: parent
acceptedButtons: Qt.RightButton
onSingleTapped: communityContextMenu.show(model.id) TapHandler {
gesturePolicy: TapHandler.ReleaseWithinBounds acceptedButtons: Qt.RightButton
onSingleTapped: communityContextMenu.show(model.id)
gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
} }
TapHandler { onClicked: Communities.setCurrentTagId(model.id)
margin: -Nheko.paddingSmall onPressAndHold: communityContextMenu.show(model.id)
onSingleTapped: Communities.setCurrentTagId(model.id)
onLongPressed: communityContextMenu.show(model.id)
}
HoverHandler {
id: hovered
margin: -Nheko.paddingSmall
}
RowLayout { RowLayout {
spacing: Nheko.paddingMedium spacing: Nheko.paddingMedium
@ -130,8 +129,9 @@ Page {
else else
return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
} }
roomid: model.id
displayName: model.displayName displayName: model.displayName
color: communityItem.background color: communityItem.backgroundColor
} }
ElidedLabel { ElidedLabel {

View File

@ -139,6 +139,7 @@ Popup {
height: popup.avatarHeight height: popup.avatarHeight
width: popup.avatarWidth width: popup.avatarWidth
displayName: model.displayName displayName: model.displayName
userid: model.userid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/") url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
onClicked: popup.completionClicked(completer.completionAt(model.index)) onClicked: popup.completionClicked(completer.completionAt(model.index))
} }
@ -194,6 +195,7 @@ Popup {
height: popup.avatarHeight height: popup.avatarHeight
width: popup.avatarWidth width: popup.avatarWidth
displayName: model.roomName displayName: model.roomName
roomid: model.roomid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/") url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
onClicked: { onClicked: {
popup.completionClicked(completer.completionAt(model.index)); popup.completionClicked(completer.completionAt(model.index));
@ -225,6 +227,7 @@ Popup {
height: popup.avatarHeight height: popup.avatarHeight
width: popup.avatarWidth width: popup.avatarWidth
displayName: model.roomName displayName: model.roomName
roomid: model.roomid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/") url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
onClicked: popup.completionClicked(completer.completionAt(model.index)) onClicked: popup.completionClicked(completer.completionAt(model.index))
} }

View File

@ -39,7 +39,7 @@ Image {
case Crypto.TOFU: case Crypto.TOFU:
return qsTr("Encrypted by an unverified device, but you have trusted that user so far."); return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
default: default:
return qsTr("Encrypted by an unverified device"); return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup.");
} }
} }

View File

@ -80,15 +80,15 @@ Popup {
completerPopup.completer.searchString = text; completerPopup.completer.searchString = text;
} }
Keys.onPressed: { Keys.onPressed: {
if (event.key == Qt.Key_Up && completerPopup.opened) { if ((event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) && completerPopup.opened) {
event.accepted = true; event.accepted = true;
completerPopup.up(); completerPopup.up();
} else if (event.key == Qt.Key_Down && completerPopup.opened) { } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
event.accepted = true; event.accepted = true;
completerPopup.down(); if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
} else if (event.key == Qt.Key_Tab && completerPopup.opened) { completerPopup.up();
event.accepted = true; else
completerPopup.down(); completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion(); completerPopup.finishCompletion();
event.accepted = true; event.accepted = true;

View File

@ -93,7 +93,7 @@ Rectangle {
TextArea { TextArea {
id: messageInput id: messageInput
property int completerTriggeredAt: -1 property int completerTriggeredAt: 0
function insertCompletion(completion) { function insertCompletion(completion) {
messageInput.remove(completerTriggeredAt, cursorPosition); messageInput.remove(completerTriggeredAt, cursorPosition);
@ -134,10 +134,9 @@ Rectangle {
return ; return ;
room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
if (cursorPosition <= completerTriggeredAt) { if (popup.opened && cursorPosition <= completerTriggeredAt)
completerTriggeredAt = -1;
popup.close(); popup.close();
}
if (popup.opened) if (popup.opened)
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)); popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
@ -145,7 +144,7 @@ Rectangle {
onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
// Ensure that we get escape key press events first. // Ensure that we get escape key press events first.
Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onShortcutOverride: event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter))
Keys.onPressed: { Keys.onPressed: {
if (event.matches(StandardKey.Paste)) { if (event.matches(StandardKey.Paste)) {
room.input.paste(false); room.input.paste(false);
@ -165,18 +164,20 @@ Rectangle {
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
messageInput.text = room.input.nextText(); messageInput.text = room.input.nextText();
} else if (event.key == Qt.Key_At) { } else if (event.key == Qt.Key_At) {
messageInput.openCompleter(cursorPosition, "user"); messageInput.openCompleter(selectionStart, "user");
popup.open(); popup.open();
} else if (event.key == Qt.Key_Colon) { } else if (event.key == Qt.Key_Colon) {
messageInput.openCompleter(cursorPosition, "emoji"); messageInput.openCompleter(selectionStart, "emoji");
popup.open(); popup.open();
} else if (event.key == Qt.Key_NumberSign) { } else if (event.key == Qt.Key_NumberSign) {
messageInput.openCompleter(cursorPosition, "roomAliases"); messageInput.openCompleter(selectionStart, "roomAliases");
popup.open(); popup.open();
} else if (event.key == Qt.Key_Escape && popup.opened) { } else if (event.key == Qt.Key_Escape && popup.opened) {
completerTriggeredAt = -1;
popup.completerName = ""; popup.completerName = "";
popup.close();
event.accepted = true; event.accepted = true;
} else if (event.matches(StandardKey.SelectAll) && popup.opened) {
popup.completerName = "";
popup.close(); popup.close();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
if (popup.opened) { if (popup.opened) {
@ -194,7 +195,10 @@ Rectangle {
} else if (event.key == Qt.Key_Tab) { } else if (event.key == Qt.Key_Tab) {
event.accepted = true; event.accepted = true;
if (popup.opened) { if (popup.opened) {
popup.up(); if (event.modifiers & Qt.ShiftModifier)
popup.down();
else
popup.up();
} else { } else {
var pos = cursorPosition - 1; var pos = cursorPosition - 1;
while (pos > -1) { while (pos > -1) {
@ -218,7 +222,7 @@ Rectangle {
} else if (event.key == Qt.Key_Up && popup.opened) { } else if (event.key == Qt.Key_Up && popup.opened) {
event.accepted = true; event.accepted = true;
popup.up(); popup.up();
} else if (event.key == Qt.Key_Down && popup.opened) { } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Backtab) && popup.opened) {
event.accepted = true; event.accepted = true;
popup.down(); popup.down();
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) { } else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
@ -264,9 +268,8 @@ Rectangle {
function onRoomChanged() { function onRoomChanged() {
messageInput.clear(); messageInput.clear();
if (room) if (room)
messageInput.append(room.input.text()); messageInput.append(room.input.text);
messageInput.completerTriggeredAt = -1;
popup.completerName = ""; popup.completerName = "";
messageInput.forceActiveFocus(); messageInput.forceActiveFocus();
} }
@ -285,8 +288,8 @@ Rectangle {
Completer { Completer {
id: popup id: popup
x: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).x : 0 x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
y: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height : 0 y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
} }
Connections { Connections {

View File

@ -23,6 +23,8 @@ ScrollView {
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2 property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
displayMarginBeginning: height / 2
displayMarginEnd: height / 2
model: room model: room
// reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
//onModelChanged: if (room) room.sendReset() //onModelChanged: if (room) room.sendReset()
@ -33,7 +35,7 @@ ScrollView {
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
onCountChanged: { onCountChanged: {
// Mark timeline as read // Mark timeline as read
if (atYEnd) if (atYEnd && room)
model.currentIndex = 0; model.currentIndex = 0;
} }
@ -233,8 +235,8 @@ ScrollView {
id: dateBubble id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: previousMessageDay !== day visible: room && previousMessageDay !== day
text: chat.model.formatDateSeparator(timestamp) text: room ? room.formatDateSeparator(timestamp) : ""
color: Nheko.colors.text color: Nheko.colors.text
height: Math.round(fontMetrics.height * 1.4) height: Math.round(fontMetrics.height * 1.4)
width: contentWidth * 1.2 width: contentWidth * 1.2
@ -257,10 +259,10 @@ ScrollView {
width: Nheko.avatarSize width: Nheko.avatarSize
height: Nheko.avatarSize height: Nheko.avatarSize
url: chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/") url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
displayName: userName displayName: userName
userid: userId userid: userId
onClicked: chat.model.openUserProfile(userId) onClicked: room.openUserProfile(userId)
ToolTip.visible: avatarHover.hovered ToolTip.visible: avatarHover.hovered
ToolTip.text: userid ToolTip.text: userid
@ -276,7 +278,7 @@ ScrollView {
} }
function onScrollToIndex(index) { function onScrollToIndex(index) {
chat.positionViewAtIndex(index, ListView.Visible); chat.positionViewAtIndex(index, ListView.Center);
} }
target: chat.model target: chat.model
@ -361,7 +363,7 @@ ScrollView {
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
width: chat.delegateMaxWidth width: chat.delegateMaxWidth
height: section ? section.height + timelinerow.height : timelinerow.height height: Math.max(section.active ? section.height + timelinerow.height : timelinerow.height, 10)
Rectangle { Rectangle {
id: scrollHighlight id: scrollHighlight
@ -420,6 +422,7 @@ ScrollView {
property string userName: wrapper.userName property string userName: wrapper.userName
property var timestamp: wrapper.timestamp property var timestamp: wrapper.timestamp
z: 4
active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day
//asynchronous: true //asynchronous: true
sourceComponent: sectionHeader sourceComponent: sectionHeader
@ -648,4 +651,39 @@ ScrollView {
} }
Platform.Menu {
id: replyContextMenu
property string text
property string link
function show(text_, link_) {
text = text_;
link = link_;
open();
}
Platform.MenuItem {
visible: replyContextMenu.text
enabled: visible
text: qsTr("&Copy")
onTriggered: Clipboard.text = replyContextMenu.text
}
Platform.MenuItem {
visible: replyContextMenu.link
enabled: visible
text: qsTr("Copy &link location")
onTriggered: Clipboard.text = replyContextMenu.link
}
Platform.MenuItem {
visible: true
enabled: visible
text: qsTr("&Go to quoted message")
onTriggered: chat.model.showEvent(eventId)
}
}
} }

View File

@ -0,0 +1,38 @@
// 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
import im.nheko 1.0
Item {
implicitHeight: warningRect.visible ? warningDisplay.implicitHeight : 0
height: implicitHeight
Layout.fillWidth: true
Rectangle {
id: warningRect
visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
color: Nheko.colors.base
anchors.fill: parent
z: 3
Label {
id: warningDisplay
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
anchors.bottom: parent.bottom
color: Nheko.theme.red
text: qsTr("You are about to notify the whole room")
textFormat: Text.PlainText
}
}
}

View File

@ -39,15 +39,15 @@ Popup {
completerPopup.completer.searchString = text; completerPopup.completer.searchString = text;
} }
Keys.onPressed: { Keys.onPressed: {
if (event.key == Qt.Key_Up && completerPopup.opened) { if ((event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) && completerPopup.opened) {
event.accepted = true; event.accepted = true;
completerPopup.up(); completerPopup.up();
} else if (event.key == Qt.Key_Down && completerPopup.opened) { } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
event.accepted = true; event.accepted = true;
completerPopup.down(); if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
} else if (event.key == Qt.Key_Tab && completerPopup.opened) { completerPopup.up();
event.accepted = true; else
completerPopup.down(); completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion(); completerPopup.finishCompletion();
event.accepted = true; event.accepted = true;

View File

@ -16,6 +16,14 @@ Page {
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
property bool collapsed: false property bool collapsed: false
Component {
id: roomDirectoryComponent
RoomDirectory {
}
}
ListView { ListView {
id: roomlist id: roomlist
@ -63,19 +71,9 @@ Page {
} }
} }
Platform.MessageDialog {
id: leaveRoomDialog
title: qsTr("Leave Room")
text: qsTr("Are you sure you want to leave this room?")
modality: Qt.ApplicationModal
onAccepted: Rooms.leave(roomContextMenu.roomid)
buttons: Dialog.Ok | Dialog.Cancel
}
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Leave room") text: qsTr("Leave room")
onTriggered: leaveRoomDialog.open() onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
} }
Platform.MenuSeparator { Platform.MenuSeparator {
@ -116,10 +114,10 @@ Page {
} }
delegate: Rectangle { delegate: ItemDelegate {
id: roomItem id: roomItem
property color background: Nheko.colors.window property color backgroundColor: Nheko.colors.window
property color importantText: Nheko.colors.text property color importantText: Nheko.colors.text
property color unimportantText: Nheko.colors.buttonText property color unimportantText: Nheko.colors.buttonText
property color bubbleBackground: Nheko.colors.highlight property color bubbleBackground: Nheko.colors.highlight
@ -135,21 +133,34 @@ Page {
required property int notificationCount required property int notificationCount
required property bool hasLoudNotification required property bool hasLoudNotification
required property bool hasUnreadMessages required property bool hasUnreadMessages
required property bool isDirect
required property string directChatOtherUserId
color: background
height: avatarSize + 2 * Nheko.paddingMedium height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width width: ListView.view.width
state: "normal" state: "normal"
ToolTip.visible: hovered.hovered && collapsed ToolTip.visible: hovered && collapsed
ToolTip.text: roomName ToolTip.text: roomName
onClicked: {
console.log("tapped " + roomId);
if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
Rooms.setCurrentRoom(roomId);
else
Rooms.resetCurrentRoom();
}
onPressAndHold: {
if (!isInvite)
roomContextMenu.show(roomId, tags);
}
states: [ states: [
State { State {
name: "highlight" name: "highlight"
when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId) when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
PropertyChanges { PropertyChanges {
target: roomItem target: roomItem
background: Nheko.colors.dark backgroundColor: Nheko.colors.dark
importantText: Nheko.colors.brightText importantText: Nheko.colors.brightText
unimportantText: Nheko.colors.brightText unimportantText: Nheko.colors.brightText
bubbleBackground: Nheko.colors.highlight bubbleBackground: Nheko.colors.highlight
@ -163,7 +174,7 @@ Page {
PropertyChanges { PropertyChanges {
target: roomItem target: roomItem
background: Nheko.colors.highlight backgroundColor: Nheko.colors.highlight
importantText: Nheko.colors.highlightedText importantText: Nheko.colors.highlightedText
unimportantText: Nheko.colors.highlightedText unimportantText: Nheko.colors.highlightedText
bubbleBackground: Nheko.colors.highlightedText bubbleBackground: Nheko.colors.highlightedText
@ -189,27 +200,6 @@ Page {
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
} }
TapHandler {
margin: -Nheko.paddingSmall
onSingleTapped: {
if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
Rooms.setCurrentRoom(roomId);
else
Rooms.resetCurrentRoom();
}
onLongPressed: {
if (!isInvite)
roomContextMenu.show(roomId, tags);
}
}
HoverHandler {
id: hovered
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
} }
RowLayout { RowLayout {
@ -229,6 +219,8 @@ Page {
width: avatarSize width: avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/") url: avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: roomName displayName: roomName
userid: isDirect ? directChatOtherUserId : ""
roomid: roomId
Rectangle { Rectangle {
id: collapsedNotificationBubble id: collapsedNotificationBubble
@ -359,6 +351,10 @@ Page {
visible: hasUnreadMessages visible: hasUnreadMessages
} }
background: Rectangle {
color: backgroundColor
}
} }
} }
@ -500,6 +496,91 @@ Page {
Layout.fillWidth: true Layout.fillWidth: true
} }
Rectangle {
id: unverifiedStuffBubble
color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1)
Layout.fillWidth: true
implicitHeight: explanation.height + Nheko.paddingMedium * 2
visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified
RowLayout {
id: unverifiedStuffBubbleContainer
width: parent.width
height: explanation.height + Nheko.paddingMedium * 2
spacing: 0
Label {
id: explanation
Layout.margins: Nheko.paddingMedium
Layout.rightMargin: Nheko.paddingSmall
color: Nheko.colors.buttonText
Layout.fillWidth: true
text: {
switch (SelfVerificationStatus.status) {
case SelfVerificationStatus.NoMasterKey:
//: Cross-signing setup has not run yet.
return qsTr("Encryption not set up");
case SelfVerificationStatus.UnverifiedMasterKey:
//: The user just signed in with this device and hasn't verified their master key.
return qsTr("Unverified login");
case SelfVerificationStatus.UnverifiedDevices:
//: There are unverified devices signed in to this account.
return qsTr("Please verify your other devices");
default:
return "";
}
}
textFormat: Text.PlainText
wrapMode: Text.Wrap
}
ImageButton {
id: closeUnverifiedBubble
Layout.rightMargin: Nheko.paddingMedium
Layout.topMargin: Nheko.paddingMedium
Layout.alignment: Qt.AlignRight | Qt.AlignTop
hoverEnabled: true
width: fontMetrics.font.pixelSize
height: fontMetrics.font.pixelSize
image: ":/icons/icons/ui/remove-symbol.png"
ToolTip.visible: closeUnverifiedBubble.hovered
ToolTip.text: qsTr("Close")
onClicked: unverifiedStuffBubble.visible = false
}
}
HoverHandler {
id: verifyButtonHovered
enabled: !closeUnverifiedBubble.hovered
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
TapHandler {
enabled: !closeUnverifiedBubble.hovered
acceptedButtons: Qt.LeftButton
onSingleTapped: {
if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
SelfVerificationStatus.verifyUnverifiedDevices();
else
SelfVerificationStatus.statusChanged();
}
}
}
Rectangle {
color: Nheko.theme.separator
height: 1
Layout.fillWidth: true
visible: unverifiedStuffBubble.visible
}
} }
footer: ColumnLayout { footer: ColumnLayout {
@ -563,6 +644,10 @@ Page {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Room directory") ToolTip.text: qsTr("Room directory")
Layout.margins: Nheko.paddingMedium Layout.margins: Nheko.paddingMedium
onClicked: {
var win = roomDirectoryComponent.createObject(timelineRoot);
win.show();
}
} }
ImageButton { ImageButton {

View File

@ -111,6 +111,30 @@ Page {
} }
Component {
id: logoutDialog
LogoutDialog {
}
}
Component {
id: joinRoomDialog
JoinRoomDialog {
}
}
Component {
id: leaveRoomComponent
LeaveRoomDialog {
}
}
Shortcut { Shortcut {
sequence: "Ctrl+K" sequence: "Ctrl+K"
onActivated: { onActivated: {
@ -120,6 +144,11 @@ Page {
} }
} }
Shortcut {
sequence: "Alt+A"
onActivated: Rooms.nextRoomWithActivity()
}
Shortcut { Shortcut {
sequence: "Ctrl+Down" sequence: "Ctrl+Down"
onActivated: Rooms.nextRoom() onActivated: Rooms.nextRoom()
@ -130,6 +159,20 @@ Page {
onActivated: Rooms.previousRoom() onActivated: Rooms.previousRoom()
} }
Connections {
function onOpenLogoutDialog() {
var dialog = logoutDialog.createObject(timelineRoot);
dialog.open();
}
function onOpenJoinRoomDialog() {
var dialog = joinRoomDialog.createObject(timelineRoot);
dialog.show();
}
target: Nheko
}
Connections { Connections {
function onNewDeviceVerificationRequest(flow) { function onNewDeviceVerificationRequest(flow) {
var dialog = deviceVerificationDialog.createObject(timelineRoot, { var dialog = deviceVerificationDialog.createObject(timelineRoot, {
@ -138,6 +181,10 @@ Page {
dialog.show(); dialog.show();
} }
target: VerificationManager
}
Connections {
function onOpenProfile(profile) { function onOpenProfile(profile) {
var userProfile = userProfileComponent.createObject(timelineRoot, { var userProfile = userProfileComponent.createObject(timelineRoot, {
"profile": profile "profile": profile
@ -176,6 +223,13 @@ Page {
dialog.show(); dialog.show();
} }
function onOpenLeaveRoomDialog(roomid) {
var dialog = leaveRoomComponent.createObject(timelineRoot, {
"roomId": roomid
});
dialog.open();
}
target: TimelineManager target: TimelineManager
} }
@ -190,6 +244,94 @@ Page {
target: CallManager target: CallManager
} }
SelfVerificationCheck {
}
InputDialog {
id: uiaPassPrompt
echoMode: TextInput.Password
title: UIA.title
prompt: qsTr("Please enter your login password to continue:")
onAccepted: (t) => {
return UIA.continuePassword(t);
}
}
InputDialog {
id: uiaEmailPrompt
title: UIA.title
prompt: qsTr("Please enter a valid email address to continue:")
onAccepted: (t) => {
return UIA.continueEmail(t);
}
}
PhoneNumberInputDialog {
id: uiaPhoneNumberPrompt
title: UIA.title
prompt: qsTr("Please enter a valid phone number to continue:")
onAccepted: (p, t) => {
return UIA.continuePhoneNumber(p, t);
}
}
InputDialog {
id: uiaTokenPrompt
title: UIA.title
prompt: qsTr("Please enter the token, which has been sent to you:")
onAccepted: (t) => {
return UIA.submit3pidToken(t);
}
}
Platform.MessageDialog {
id: uiaErrorDialog
buttons: Platform.MessageDialog.Ok
}
Platform.MessageDialog {
id: uiaConfirmationLinkDialog
buttons: Platform.MessageDialog.Ok
text: qsTr("Wait for the confirmation link to arrive, then continue.")
onAccepted: UIA.continue3pidReceived()
}
Connections {
function onPassword() {
console.log("UIA: password needed");
uiaPassPrompt.show();
}
function onEmail() {
uiaEmailPrompt.show();
}
function onPhoneNumber() {
uiaPhoneNumberPrompt.show();
}
function onPrompt3pidToken() {
uiaTokenPrompt.show();
}
function onConfirm3pidToken() {
uiaConfirmationLinkDialog.open();
}
function onError(msg) {
uiaErrorDialog.text = msg;
uiaErrorDialog.open();
}
target: UIA
}
ChatPage { ChatPage {
anchors.fill: parent anchors.fill: parent
} }

View File

@ -0,0 +1,305 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./components/"
import Qt.labs.platform 1.1 as P
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
import im.nheko 1.0
Item {
visible: false
enabled: false
Dialog {
id: showRecoverKeyDialog
property string recoveryKey: ""
parent: Overlay.overlay
anchors.centerIn: parent
height: content.height + implicitFooterHeight + implicitHeaderHeight
width: content.width
padding: 0
modal: true
standardButtons: Dialog.Ok
closePolicy: Popup.NoAutoClose
ColumnLayout {
id: content
spacing: 0
Label {
Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
Layout.fillWidth: true
text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Do not pass go! Do not collect $200!")
color: Nheko.colors.text
wrapMode: Text.Wrap
}
TextEdit {
Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: TextEdit.AlignHCenter
verticalAlignment: TextEdit.AlignVCenter
readOnly: true
selectByMouse: true
text: showRecoverKeyDialog.recoveryKey
color: Nheko.colors.text
font.bold: true
wrapMode: TextEdit.Wrap
}
}
background: Rectangle {
color: Nheko.colors.window
border.color: Nheko.theme.separator
border.width: 1
radius: Nheko.paddingSmall
}
}
P.MessageDialog {
id: successDialog
buttons: P.MessageDialog.Ok
text: qsTr("Encryption setup successfully")
}
P.MessageDialog {
id: failureDialog
property string errorMessage
buttons: P.MessageDialog.Ok
text: qsTr("Failed to setup encryption: %1").arg(errorMessage)
}
MainWindowDialog {
id: bootstrapCrosssigning
onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked)
GridLayout {
id: grid
width: bootstrapCrosssigning.useableWidth
columns: 2
rowSpacing: 0
columnSpacing: 0
z: 1
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignHCenter
Layout.columnSpan: 2
font.pointSize: fontMetrics.font.pointSize * 2
text: qsTr("Setup Encryption")
color: Nheko.colors.text
wrapMode: Text.Wrap
}
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 2
Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!")
color: Nheko.colors.text
wrapMode: Text.Wrap
}
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 1
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!"
color: Nheko.colors.text
wrapMode: Text.Wrap
}
Item {
Layout.margins: Nheko.paddingMedium
Layout.preferredHeight: storeSecretsOnline.height
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
ToggleButton {
id: storeSecretsOnline
checked: true
onClicked: console.log("Store secrets toggled: " + checked)
}
}
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 1
Layout.rowSpan: 2
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
visible: storeSecretsOnline.checked
text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)"
color: Nheko.colors.text
wrapMode: Text.Wrap
}
Item {
Layout.margins: Nheko.paddingMedium
Layout.topMargin: Nheko.paddingLarge
Layout.preferredHeight: storeSecretsOnline.height
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.rowSpan: usePassword.checked ? 1 : 2
Layout.fillWidth: true
visible: storeSecretsOnline.checked
ToggleButton {
id: usePassword
checked: false
}
}
MatrixTextField {
id: passwordField
Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.columnSpan: 1
Layout.fillWidth: true
visible: storeSecretsOnline.checked && usePassword.checked
echoMode: TextInput.Password
}
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 1
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages."
color: Nheko.colors.text
wrapMode: Text.Wrap
}
Item {
Layout.margins: Nheko.paddingMedium
Layout.preferredHeight: storeSecretsOnline.height
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
ToggleButton {
id: useOnlineKeyBackup
checked: true
onClicked: console.log("Online key backup toggled: " + checked)
}
}
}
background: Rectangle {
color: Nheko.colors.window
border.color: Nheko.theme.separator
border.width: 1
radius: Nheko.paddingSmall
}
}
MainWindowDialog {
id: verifyMasterKey
standardButtons: Dialog.Cancel
GridLayout {
id: masterGrid
width: verifyMasterKey.useableWidth
columns: 1
z: 1
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignHCenter
//Layout.columnSpan: 2
font.pointSize: fontMetrics.font.pointSize * 2
text: qsTr("Activate Encryption")
color: Nheko.colors.text
wrapMode: Text.Wrap
}
Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft
//Layout.columnSpan: 2
Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.")
color: Nheko.colors.text
wrapMode: Text.Wrap
}
FlatButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("verify")
onClicked: {
SelfVerificationStatus.verifyMasterKey();
verifyMasterKey.close();
}
}
FlatButton {
visible: SelfVerificationStatus.hasSSSS
Layout.alignment: Qt.AlignHCenter
text: qsTr("enter passphrase")
onClicked: {
SelfVerificationStatus.verifyMasterKeyWithPassphrase();
verifyMasterKey.close();
}
}
}
}
Connections {
function onStatusChanged() {
console.log("STATUS CHANGED: " + SelfVerificationStatus.status);
if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) {
bootstrapCrosssigning.open();
} else if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedMasterKey) {
verifyMasterKey.open();
} else {
bootstrapCrosssigning.close();
verifyMasterKey.close();
}
}
function onShowRecoveryKey(key) {
showRecoverKeyDialog.recoveryKey = key;
showRecoverKeyDialog.open();
}
function onSetupCompleted() {
successDialog.open();
}
function onSetupFailed(m) {
failureDialog.errorMessage = m;
failureDialog.open();
}
target: SelfVerificationStatus
}
}

View File

@ -24,7 +24,7 @@ Item {
property bool showBackButton: false property bool showBackButton: false
Label { Label {
visible: !room && !TimelineManager.isInitialSync && !roomPreview visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
anchors.centerIn: parent anchors.centerIn: parent
text: qsTr("No room open") text: qsTr("No room open")
font.pointSize: 24 font.pointSize: 24
@ -84,14 +84,9 @@ Item {
target: timelineView target: timelineView
} }
Loader { MessageView {
active: room || roomPreview implicitHeight: msgView.height - typingIndicator.height
Layout.fillWidth: true Layout.fillWidth: true
sourceComponent: MessageView {
implicitHeight: msgView.height - typingIndicator.height
}
} }
Loader { Loader {
@ -128,6 +123,9 @@ Item {
color: Nheko.theme.separator color: Nheko.theme.separator
} }
NotificationWarning {
}
ReplyPopup { ReplyPopup {
} }
@ -139,6 +137,7 @@ Item {
ColumnLayout { ColumnLayout {
id: preview id: preview
property string roomId: room ? room.roomId : (roomPreview ? roomPreview.roomid : "")
property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "") property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "")
property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "") property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "")
property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "") property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "")
@ -155,6 +154,7 @@ Item {
Avatar { Avatar {
url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: parent.roomId
displayName: parent.roomName displayName: parent.roomName
height: 130 height: 130
width: 130 width: 130
@ -163,7 +163,7 @@ Item {
} }
MatrixText { MatrixText {
text: parent.roomName text: parent.roomName == "" ? qsTr("No preview available") : parent.roomName
font.pixelSize: 24 font.pixelSize: 24
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
@ -240,7 +240,7 @@ Item {
anchors.margins: Nheko.paddingMedium anchors.margins: Nheko.paddingMedium
width: Nheko.avatarSize width: Nheko.avatarSize
height: Nheko.avatarSize height: Nheko.avatarSize
visible: room != null && room.isSpace && showBackButton visible: (room == null || room.isSpace) && showBackButton
enabled: visible enabled: visible
image: ":/icons/icons/ui/angle-pointing-to-left.png" image: ":/icons/icons/ui/angle-pointing-to-left.png"
ToolTip.visible: hovered ToolTip.visible: hovered

View File

@ -13,10 +13,13 @@ Rectangle {
property bool showBackButton: false property bool showBackButton: false
property string roomName: room ? room.roomName : qsTr("No room selected") property string roomName: room ? room.roomName : qsTr("No room selected")
property string roomId: room ? room.roomId : ""
property string avatarUrl: room ? room.roomAvatarUrl : "" property string avatarUrl: room ? room.roomAvatarUrl : ""
property string roomTopic: room ? room.roomTopic : "" property string roomTopic: room ? room.roomTopic : ""
property bool isEncrypted: room ? room.isEncrypted : false property bool isEncrypted: room ? room.isEncrypted : false
property int trustlevel: room ? room.trustlevel : Crypto.Unverified property int trustlevel: room ? room.trustlevel : Crypto.Unverified
property bool isDirect: room ? room.isDirect : false
property string directChatOtherUserId: room ? room.directChatOtherUserId : ""
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: topLayout.height + Nheko.paddingMedium * 2 implicitHeight: topLayout.height + Nheko.paddingMedium * 2
@ -65,10 +68,12 @@ Rectangle {
width: Nheko.avatarSize width: Nheko.avatarSize
height: Nheko.avatarSize height: Nheko.avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/") url: avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: roomId
userid: isDirect ? directChatOtherUserId : ""
displayName: roomName displayName: roomName
onClicked: { onClicked: {
if (room) if (room)
TimelineManager.openRoomSettings(room.roomId); TimelineManager.openRoomSettings(roomId);
} }
} }
@ -109,7 +114,7 @@ Rectangle {
case Crypto.Verified: case Crypto.Verified:
return qsTr("This room contains only verified devices."); return qsTr("This room contains only verified devices.");
case Crypto.TOFU: case Crypto.TOFU:
return qsTr("This rooms contain verified devices and devices which have never changed their master key."); return qsTr("This room contains verified devices and devices which have never changed their master key.");
default: default:
return qsTr("This room contains unverified devices!"); return qsTr("This room contains unverified devices!");
} }
@ -135,7 +140,7 @@ Rectangle {
Platform.MenuItem { Platform.MenuItem {
visible: room ? room.permissions.canInvite() : false visible: room ? room.permissions.canInvite() : false
text: qsTr("Invite users") text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsers(room.roomId) onTriggered: TimelineManager.openInviteUsers(roomId)
} }
Platform.MenuItem { Platform.MenuItem {
@ -145,12 +150,12 @@ Rectangle {
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Leave room") text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId) onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Settings") text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings(room.roomId) onTriggered: TimelineManager.openRoomSettings(roomId)
} }
} }

View File

@ -1,270 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./device-verification"
import "./ui"
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
import im.nheko 1.0
ApplicationWindow {
// this does not work in ApplicationWindow, just in Window
//transientParent: Nheko.mainwindow()
id: userProfileDialog
property var profile
height: 650
width: 420
minimumHeight: 420
palette: Nheko.colors
color: Nheko.colors.window
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint
Component.onCompleted: Nheko.reparent(userProfileDialog)
Shortcut {
sequence: StandardKey.Cancel
onActivated: userProfileDialog.close()
}
ColumnLayout {
id: contentL
anchors.fill: parent
anchors.margins: 10
spacing: 10
Avatar {
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
height: 130
width: 130
displayName: profile.displayName
userid: profile.userid
Layout.alignment: Qt.AlignHCenter
onClicked: profile.isSelf ? profile.changeAvatar() : TimelineManager.openImageOverlay(profile.avatarUrl, "")
}
Spinner {
Layout.alignment: Qt.AlignHCenter
running: profile.isLoading
visible: profile.isLoading
foreground: Nheko.colors.mid
}
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 {
function onDisplayError(errorMessage) {
errorText.text = errorMessage;
errorText.opacity = 1;
hideErrorAnimation.restart();
}
target: profile
}
TextInput {
id: displayUsername
property bool isUsernameEditingAllowed
readOnly: !isUsernameEditingAllowed
text: profile.displayName
font.pixelSize: 20
color: TimelineManager.userColor(profile.userid, Nheko.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 {
text: profile.userid
font.pixelSize: 15
Layout.alignment: Qt.AlignHCenter
}
Button {
id: verifyUserButton
text: qsTr("Verify")
Layout.alignment: Qt.AlignHCenter
enabled: profile.userVerified != Crypto.Verified
visible: profile.userVerified != Crypto.Verified && !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" : Nheko.colors.buttonText)
visible: profile.userVerified != Crypto.Unverified
Layout.alignment: Qt.AlignHCenter
}
RowLayout {
// ImageButton{
// image:":/icons/icons/ui/volume-off-indicator.png"
// Layout.margins: {
// left: 5
// right: 5
// }
// ToolTip.visible: hovered
// ToolTip.text: qsTr("Ignore messages from this user")
// onClicked : {
// profile.ignoreUser()
// }
// }
Layout.alignment: Qt.AlignHCenter
spacing: 8
ImageButton {
image: ":/icons/icons/ui/black-bubble-speech.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Start a private chat")
onClicked: profile.startChat()
}
ImageButton {
image: ":/icons/icons/ui/round-remove-button.png"
hoverEnabled: true
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
}
}
ListView {
id: devicelist
Layout.fillHeight: true
Layout.minimumHeight: 200
Layout.fillWidth: true
clip: true
spacing: 8
boundsBehavior: Flickable.StopAtBounds
model: profile.deviceList
delegate: RowLayout {
width: devicelist.width
spacing: 4
ColumnLayout {
spacing: 0
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
elide: Text.ElideRight
font.bold: true
color: Nheko.colors.text
text: model.deviceId
}
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
elide: Text.ElideRight
color: Nheko.colors.text
text: model.deviceName
}
}
Image {
Layout.preferredHeight: 16
Layout.preferredWidth: 16
source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red"))
}
Button {
id: verifyButton
visible: (!profile.userVerificationEnabled && !profile.isSelf) || (profile.isSelf && (model.verificationStatus != VerificationStatus.VERIFIED || !profile.userVerificationEnabled))
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
onClicked: {
if (model.verificationStatus == VerificationStatus.VERIFIED)
profile.unverify(model.deviceId);
else
profile.verify(model.deviceId);
}
}
}
}
}
footer: DialogButtonBox {
standardButtons: DialogButtonBox.Ok
onAccepted: userProfileDialog.close()
}
}

View File

@ -23,6 +23,8 @@ Rectangle {
required property int index required property int index
required property int selectedIndex required property int selectedIndex
property bool crop: true property bool crop: true
property alias roomid: avatar.roomid
property alias userid: avatar.userid
color: background color: background
height: avatarSize + 2 * Nheko.paddingMedium height: avatarSize + 2 * Nheko.paddingMedium

View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as P
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
import im.nheko 1.0
Dialog {
default property alias inner: scroll.data
property int useableWidth: scroll.width - scroll.ScrollBar.vertical.width
parent: Overlay.overlay
anchors.centerIn: parent
height: (Math.floor(parent.height / 2) - Nheko.paddingLarge) * 2
width: (Math.floor(parent.width / 2) - Nheko.paddingLarge) * 2
padding: 0
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
closePolicy: Popup.NoAutoClose
contentChildren: [
ScrollView {
id: scroll
clip: true
anchors.fill: parent
ScrollBar.horizontal.visible: false
ScrollBar.vertical.visible: true
}
]
background: Rectangle {
color: Nheko.colors.window
border.color: Nheko.theme.separator
border.width: 1
radius: Nheko.paddingSmall
}
}

View File

@ -3,11 +3,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import ".." import ".."
import QtQuick 2.15
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.2
import im.nheko 1.0 import im.nheko 1.0
ColumnLayout { Column {
id: r id: r
required property int encryptionError required property int encryptionError

View File

@ -14,6 +14,7 @@ Item {
required property string body required property string body
required property string filename required property string filename
required property bool isReply required property bool isReply
required property string eventId
property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth) property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth)
property double tempHeight: tempWidth * proportionalHeight property double tempHeight: tempWidth * proportionalHeight
property double divisor: isReply ? 5 : 3 property double divisor: isReply ? 5 : 3
@ -37,6 +38,7 @@ Item {
Image { Image {
id: img id: img
visible: !mxcimage.loaded
anchors.fill: parent anchors.fill: parent
source: url.replace("mxc://", "image://MxcImage/") source: url.replace("mxc://", "image://MxcImage/")
asynchronous: true asynchronous: true
@ -53,38 +55,48 @@ Item {
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
} }
HoverHandler { }
id: mouseArea
MxcAnimatedImage {
id: mxcimage
visible: loaded
anchors.fill: parent
roomm: room
play: !Settings.animateImagesOnHover || mouseArea.hovered
eventId: parent.eventId
}
HoverHandler {
id: mouseArea
}
Item {
id: overlay
anchors.fill: parent
visible: mouseArea.hovered
Rectangle {
id: container
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: Nheko.colors.window
opacity: 0.75
} }
Item { Text {
id: overlay id: imgcaption
anchors.fill: parent
visible: mouseArea.hovered
Rectangle {
id: container
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: Nheko.colors.window
opacity: 0.75
}
Text {
id: imgcaption
anchors.fill: container
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: filename ? filename : body
color: Nheko.colors.text
}
anchors.fill: container
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: filename ? filename : body
color: Nheko.colors.text
} }
} }

View File

@ -3,6 +3,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.6 import QtQuick 2.6
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.2
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
@ -32,7 +34,7 @@ Item {
required property int encryptionError required property int encryptionError
required property int relatedEventCacheBuster required property int relatedEventCacheBuster
height: chooser.childrenRect.height height: Math.max(chooser.child.height, 20)
DelegateChooser { DelegateChooser {
id: chooser id: chooser
@ -100,6 +102,7 @@ Item {
body: d.body body: d.body
filename: d.filename filename: d.filename
isReply: d.isReply isReply: d.isReply
eventId: d.eventId
} }
} }
@ -116,6 +119,7 @@ Item {
body: d.body body: d.body
filename: d.filename filename: d.filename
isReply: d.isReply isReply: d.isReply
eventId: d.eventId
} }
} }
@ -357,11 +361,23 @@ Item {
DelegateChoice { DelegateChoice {
roleValue: MtxEvent.Member roleValue: MtxEvent.Member
NoticeMessage { ColumnLayout {
body: formatted width: parent ? parent.width : undefined
isOnlyEmoji: false
isReply: d.isReply NoticeMessage {
formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId) body: formatted
isOnlyEmoji: false
isReply: d.isReply
formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId)
}
Button {
visible: d.relatedEventCacheBuster, room.showAcceptKnockButton(d.eventId)
palette: Nheko.colors
text: qsTr("Allow them in")
onClicked: room.acceptKnock(eventId)
}
} }
} }

View File

@ -3,9 +3,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import "../" import "../"
import QtMultimedia 5.6 import QtMultimedia 5.15
import QtQuick 2.12 import QtQuick 2.15
import QtQuick.Controls 2.1 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import im.nheko 1.0 import im.nheko 1.0
@ -40,26 +40,16 @@ ColumnLayout {
id: content id: content
Layout.maximumWidth: parent? parent.width: undefined Layout.maximumWidth: parent? parent.width: undefined
MediaPlayer { MxcMedia {
id: media id: mxcmedia
// TODO: Show error in overlay or so? // TODO: Show error in overlay or so?
onError: console.log(errorString) onError: console.log(error)
volume: volumeSlider.desiredVolume roomm: room
} onMediaStatusChanged: {
if (status == MxcMedia.LoadedMedia) {
Connections { progress.updatePositionTexts();
property bool mediaCached: false }
}
id: mediaCachedObserver
target: room
function onMediaCached(mxcUrl, cacheUrl) {
if (mxcUrl == url) {
mediaCached = true
media.source = "file://" + cacheUrl
console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
}
console.log("media cached: " + mxcUrl + " at " + cacheUrl)
}
} }
Rectangle { Rectangle {
@ -87,7 +77,7 @@ ColumnLayout {
Rectangle { Rectangle {
// Display over video controls // Display over video controls
z: videoOutput.z + 1 z: videoOutput.z + 1
visible: !mediaCachedObserver.mediaCached visible: !mxcmedia.loaded
anchors.fill: parent anchors.fill: parent
color: Nheko.colors.window color: Nheko.colors.window
opacity: 0.5 opacity: 0.5
@ -103,8 +93,8 @@ ColumnLayout {
id: cacheVideoArea id: cacheVideoArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
enabled: !mediaCachedObserver.mediaCached enabled: !mxcmedia.loaded
onClicked: room.cacheMedia(eventId) onClicked: mxcmedia.eventId = eventId
} }
} }
VideoOutput { VideoOutput {
@ -112,7 +102,9 @@ ColumnLayout {
clip: true clip: true
anchors.fill: parent anchors.fill: parent
fillMode: VideoOutput.PreserveAspectFit fillMode: VideoOutput.PreserveAspectFit
source: media source: mxcmedia
flushMode: VideoOutput.FirstFrame
// TODO: once we can use Qt 5.12, use HoverHandler // TODO: once we can use Qt 5.12, use HoverHandler
MouseArea { MouseArea {
id: playerMouseArea id: playerMouseArea
@ -120,9 +112,9 @@ ColumnLayout {
onClicked: { onClicked: {
if (controlRect.shouldShowControls && if (controlRect.shouldShowControls &&
!controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) { !controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) {
(media.playbackState == MediaPlayer.PlayingState) ? (mxcmedia.state == MediaPlayer.PlayingState) ?
media.pause() : mxcmedia.pause() :
media.play() mxcmedia.play()
} }
} }
Rectangle { Rectangle {
@ -159,7 +151,7 @@ ColumnLayout {
property color controlColor: (playbackStateArea.containsMouse) ? property color controlColor: (playbackStateArea.containsMouse) ?
Nheko.colors.highlight : Nheko.colors.text Nheko.colors.highlight : Nheko.colors.text
source: (media.playbackState == MediaPlayer.PlayingState) ? source: (mxcmedia.state == MediaPlayer.PlayingState) ?
"image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor : "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor :
"image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor "image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor
MouseArea { MouseArea {
@ -168,25 +160,25 @@ ColumnLayout {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
(media.playbackState == MediaPlayer.PlayingState) ? (mxcmedia.state == MediaPlayer.PlayingState) ?
media.pause() : mxcmedia.pause() :
media.play() mxcmedia.play()
} }
} }
} }
Label { Label {
text: (!mediaCachedObserver.mediaCached) ? "-/-" : text: (!mxcmedia.loaded) ? "-/-" :
durationToString(media.position) + "/" + durationToString(media.duration) durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
} }
Slider { Slider {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 50 Layout.minimumWidth: 50
height: controlRect.controlHeight height: controlRect.controlHeight
value: media.position value: mxcmedia.position
onMoved: media.seek(value) onMoved: mxcmedia.position = value
from: 0 from: 0
to: media.duration to: mxcmedia.duration
} }
// Volume slider activator // Volume slider activator
Image { Image {
@ -195,7 +187,7 @@ ColumnLayout {
// TODO: add icons for different volume levels // TODO: add icons for different volume levels
id: volumeImage id: volumeImage
source: (media.volume > 0 && !media.muted) ? source: (mxcmedia.volume > 0 && !mxcmedia.muted) ?
"image://colorimage/:/icons/icons/ui/volume-up.png?"+ controlColor : "image://colorimage/:/icons/icons/ui/volume-up.png?"+ controlColor :
"image://colorimage/:/icons/icons/ui/volume-off-indicator.png?"+ controlColor "image://colorimage/:/icons/icons/ui/volume-off-indicator.png?"+ controlColor
Layout.rightMargin: 5 Layout.rightMargin: 5
@ -205,7 +197,7 @@ ColumnLayout {
id: volumeImageArea id: volumeImageArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onClicked: media.muted = !media.muted onClicked: mxcmedia.muted = !mxcmedia.muted
onExited: volumeSliderHideTimer.start() onExited: volumeSliderHideTimer.start()
onPositionChanged: volumeSliderHideTimer.start() onPositionChanged: volumeSliderHideTimer.start()
// For hiding volume slider after a while // For hiding volume slider after a while
@ -248,7 +240,7 @@ ColumnLayout {
id: volumeSlider id: volumeSlider
from: 0 from: 0
to: 1 to: 1
value: (media.muted) ? 0 : value: (mxcmedia.muted) ? 0 :
QtMultimedia.convertVolume(desiredVolume, QtMultimedia.convertVolume(desiredVolume,
QtMultimedia.LinearVolumeScale, QtMultimedia.LinearVolumeScale,
QtMultimedia.LogarithmicVolumeScale) QtMultimedia.LogarithmicVolumeScale)
@ -262,7 +254,7 @@ ColumnLayout {
QtMultimedia.LinearVolumeScale) QtMultimedia.LinearVolumeScale)
/* This would be better handled in 'media', but it has some issue with listening /* This would be better handled in 'media', but it has some issue with listening
to this signal */ to this signal */
onDesiredVolumeChanged: media.muted = !(desiredVolume > 0) onDesiredVolumeChanged: mxcmedia.muted = !(desiredVolume > 0)
} }
// Used for resetting the timer on mouse moves on volumeSliderRect // Used for resetting the timer on mouse moves on volumeSliderRect
MouseArea { MouseArea {
@ -288,7 +280,7 @@ ColumnLayout {
} }
// This breaks separation of concerns but this same thing doesn't work when called from controlRect... // This breaks separation of concerns but this same thing doesn't work when called from controlRect...
property bool shouldShowControls: (containsMouse && controlHideTimer.running) || property bool shouldShowControls: (containsMouse && controlHideTimer.running) ||
(media.playbackState != MediaPlayer.PlayingState) || (mxcmedia.state != MediaPlayer.PlayingState) ||
controlRect.contains(mapToItem(controlRect, mouseX, mouseY)) controlRect.contains(mapToItem(controlRect, mouseX, mouseY))
// For hiding controls on stationary cursor // For hiding controls on stationary cursor
@ -331,9 +323,9 @@ ColumnLayout {
Nheko.colors.highlight : Nheko.colors.text Nheko.colors.highlight : Nheko.colors.text
source: { source: {
if (!mediaCachedObserver.mediaCached) if (!mxcmedia.loaded)
return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+controlColor return "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+controlColor
return (media.playbackState == MediaPlayer.PlayingState) ? return (mxcmedia.state == MediaPlayer.PlayingState) ?
"image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor : "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+controlColor :
"image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor "image://colorimage/:/icons/icons/ui/play-sign.png?"+controlColor
} }
@ -343,29 +335,29 @@ ColumnLayout {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
if (!mediaCachedObserver.mediaCached) { if (!mxcmedia.loaded) {
room.cacheMedia(eventId) mxcmedia.eventId = eventId
return return
} }
(media.playbackState == MediaPlayer.PlayingState) ? (mxcmedia.state == MediaPlayer.PlayingState) ?
media.pause() : mxcmedia.pause() :
media.play() mxcmedia.play()
} }
} }
} }
Label { Label {
text: (!mediaCachedObserver.mediaCached) ? "-/-" : text: (!mxcmedia.loaded) ? "-/-" :
durationToString(media.position) + "/" + durationToString(media.duration) durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
} }
Slider { Slider {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 50 Layout.minimumWidth: 50
height: controlRect.controlHeight height: controlRect.controlHeight
value: media.position value: mxcmedia.position
onMoved: media.seek(value) onMoved: mxcmedia.seek(value)
from: 0 from: 0
to: media.duration to: mxcmedia.duration
} }
} }
} }

View File

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
@ -36,11 +37,6 @@ Item {
width: parent.width width: parent.width
height: replyContainer.height height: replyContainer.height
TapHandler {
onSingleTapped: chat.model.showEvent(eventId)
gesturePolicy: TapHandler.ReleaseWithinBounds
}
CursorShape { CursorShape {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -62,6 +58,19 @@ Item {
anchors.leftMargin: 4 anchors.leftMargin: 4
width: parent.width - 8 width: parent.width - 8
TapHandler {
acceptedButtons: Qt.LeftButton
onSingleTapped: chat.model.showEvent(r.eventId)
gesturePolicy: TapHandler.ReleaseWithinBounds
}
TapHandler {
acceptedButtons: Qt.RightButton
onLongPressed: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
gesturePolicy: TapHandler.ReleaseWithinBounds
}
Text { Text {
id: userName_ id: userName_
@ -99,6 +108,7 @@ Item {
callType: r.callType callType: r.callType
relatedEventCacheBuster: r.relatedEventCacheBuster relatedEventCacheBuster: r.relatedEventCacheBuster
encryptionError: r.encryptionError encryptionError: r.encryptionError
// This is disabled so that left clicking the reply goes to its location
enabled: false enabled: false
width: parent.width width: parent.width
isReply: true isReply: true

View File

@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import ".." import ".."
import QtQuick.Controls 2.3
import im.nheko 1.0 import im.nheko 1.0
MatrixText { MatrixText {
@ -28,6 +29,7 @@ MatrixText {
border-collapse: collapse; border-collapse: collapse;
border: 1px solid " + Nheko.colors.text + "; border: 1px solid " + Nheko.colors.text + ";
} }
blockquote { margin-left: 1em; }
</style> </style>
" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>").replace("<del>", "<s>").replace("</del>", "</s>").replace("<strike>", "<s>").replace("</strike>", "</s>") " + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>").replace("<del>", "<s>").replace("</del>", "</s>").replace("<strike>", "<s>").replace("</strike>", "</s>")
width: parent ? parent.width : undefined width: parent ? parent.width : undefined
@ -35,4 +37,11 @@ MatrixText {
clip: isReply clip: isReply
selectByMouse: !Settings.mobileMode && !isReply selectByMouse: !Settings.mobileMode && !isReply
font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
CursorShape {
enabled: isReply
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
} }

View File

@ -12,13 +12,13 @@ ApplicationWindow {
property var flow property var flow
onClosing: TimelineManager.removeVerificationFlow(flow) onClosing: VerificationManager.removeVerificationFlow(flow)
title: stack.currentItem.title title: stack.currentItem.title
modality: Qt.NonModal modality: Qt.NonModal
palette: Nheko.colors palette: Nheko.colors
height: stack.implicitHeight height: stack.implicitHeight
width: stack.implicitWidth width: stack.implicitWidth
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(dialog) Component.onCompleted: Nheko.reparent(dialog)
StackView { StackView {

View File

@ -33,9 +33,9 @@ Pane {
case DeviceVerificationFlow.User: case DeviceVerificationFlow.User:
return qsTr("Other party canceled the verification."); return qsTr("Other party canceled the verification.");
case DeviceVerificationFlow.OutOfOrder: case DeviceVerificationFlow.OutOfOrder:
return qsTr("Device verification timed out."); return qsTr("Verification messages received out of order!");
default: default:
return "Unknown verification error."; return qsTr("Unknown verification error.");
} }
} }
color: Nheko.colors.text color: Nheko.colors.text

View File

@ -23,7 +23,10 @@ Pane {
text: { text: {
if (flow.sender) { if (flow.sender) {
if (flow.isSelfVerification) if (flow.isSelfVerification)
return qsTr("To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?").arg(flow.deviceId); if (flow.isMultiDeviceVerification)
return qsTr("To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify an unverified device now? (Please make sure you have one of those devices available.)");
else
return qsTr("To allow other users to see, which of your devices actually belong to you, you can verify them. This also allows key backup to work automatically. Verify %1 now?").arg(flow.deviceId);
else else
return qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party."); return qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.");
} else { } else {

View File

@ -27,7 +27,7 @@ ApplicationWindow {
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.base color: Nheko.colors.base
modality: Qt.WindowModal modality: Qt.WindowModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
AdaptiveLayout { AdaptiveLayout {
id: adaptiveView id: adaptiveView
@ -61,6 +61,7 @@ ApplicationWindow {
header: AvatarListTile { header: AvatarListTile {
title: imagePack.packname title: imagePack.packname
avatarUrl: imagePack.avatarUrl avatarUrl: imagePack.avatarUrl
roomid: imagePack.statekey
subtitle: imagePack.statekey subtitle: imagePack.statekey
index: -1 index: -1
selectedIndex: currentImageIndex selectedIndex: currentImageIndex
@ -90,7 +91,7 @@ ApplicationWindow {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
fileMode: FileDialog.OpenFiles fileMode: FileDialog.OpenFiles
nameFilters: [qsTr("Stickers (*.png *.webp)")] nameFilters: [qsTr("Stickers (*.png *.webp *.gif *.jpg *.jpeg)")]
onAccepted: imagePack.addStickers(files) onAccepted: imagePack.addStickers(files)
} }
@ -142,6 +143,7 @@ ApplicationWindow {
Layout.columnSpan: 2 Layout.columnSpan: 2
url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/") url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: imagePack.packname displayName: imagePack.packname
roomid: imagePack.statekey
height: 130 height: 130
width: 130 width: 130
crop: false crop: false
@ -219,6 +221,7 @@ ApplicationWindow {
Layout.columnSpan: 2 Layout.columnSpan: 2
url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/") url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/")
displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
roomid: displayName
height: 130 height: 130
width: 130 width: 130
crop: false crop: false
@ -265,6 +268,20 @@ ApplicationWindow {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
} }
MatrixText {
text: qsTr("Remove from pack")
}
Button {
text: qsTr("Remove")
onClicked: {
let temp = currentImageIndex;
currentImageIndex = -1;
imagePack.remove(temp);
}
Layout.alignment: Qt.AlignRight
}
Item { Item {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillHeight: true Layout.fillHeight: true

View File

@ -25,7 +25,7 @@ ApplicationWindow {
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.base color: Nheko.colors.base
modality: Qt.NonModal modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(win) Component.onCompleted: Nheko.reparent(win)
Component { Component {
@ -101,6 +101,7 @@ ApplicationWindow {
required property string displayName required property string displayName
required property bool fromAccountData required property bool fromAccountData
required property bool fromCurrentRoom required property bool fromCurrentRoom
required property string statekey
title: displayName title: displayName
subtitle: { subtitle: {
@ -112,6 +113,7 @@ ApplicationWindow {
return qsTr("Globally enabled pack"); return qsTr("Globally enabled pack");
} }
selectedIndex: currentPackIndex selectedIndex: currentPackIndex
roomid: statekey
TapHandler { TapHandler {
onSingleTapped: currentPackIndex = index onSingleTapped: currentPackIndex = index
@ -135,6 +137,7 @@ ApplicationWindow {
property string packName: currentPack ? currentPack.packname : "" property string packName: currentPack ? currentPack.packname : ""
property string attribution: currentPack ? currentPack.attribution : "" property string attribution: currentPack ? currentPack.attribution : ""
property string avatarUrl: currentPack ? currentPack.avatarUrl : "" property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
property string statekey: currentPack ? currentPack.statekey : ""
anchors.fill: parent anchors.fill: parent
anchors.margins: Nheko.paddingLarge anchors.margins: Nheko.paddingLarge
@ -143,6 +146,7 @@ ApplicationWindow {
Avatar { Avatar {
url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/") url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: packinfo.packName displayName: packinfo.packName
roomid: packinfo.statekey
height: 100 height: 100
width: 100 width: 100
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter

View File

@ -12,6 +12,7 @@ ApplicationWindow {
id: inputDialog id: inputDialog
property alias prompt: promptLabel.text property alias prompt: promptLabel.text
property alias echoMode: statusInput.echoMode
property var onAccepted: undefined property var onAccepted: undefined
modality: Qt.NonModal modality: Qt.NonModal
@ -21,7 +22,8 @@ ApplicationWindow {
height: fontMetrics.lineSpacing * 7 height: fontMetrics.lineSpacing * 7
ColumnLayout { ColumnLayout {
anchors.margins: Nheko.paddingLarge spacing: Nheko.paddingMedium
anchors.margins: Nheko.paddingMedium
anchors.fill: parent anchors.fill: parent
Label { Label {

View File

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
@ -34,7 +35,7 @@ ApplicationWindow {
width: 340 width: 340
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.window color: Nheko.colors.window
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(inviteDialogRoot) Component.onCompleted: Nheko.reparent(inviteDialogRoot)
Shortcut { Shortcut {

View File

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3
import im.nheko 1.0
ApplicationWindow {
id: joinRoomRoot
title: qsTr("Join room")
modality: Qt.WindowModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
palette: Nheko.colors
color: Nheko.colors.window
Component.onCompleted: Nheko.reparent(joinRoomRoot)
width: 350
height: fontMetrics.lineSpacing * 7
Shortcut {
sequence: StandardKey.Cancel
onActivated: dbb.rejected()
}
ColumnLayout {
spacing: Nheko.paddingMedium
anchors.margins: Nheko.paddingMedium
anchors.fill: parent
Label {
id: promptLabel
text: qsTr("Room ID or alias")
color: Nheko.colors.text
}
MatrixTextField {
id: input
focus: true
Layout.fillWidth: true
onAccepted: {
if (input.text.match("#.+?:.{3,}"))
dbb.accepted();
}
}
}
footer: DialogButtonBox {
id: dbb
onAccepted: {
Nheko.joinRoom(input.text);
joinRoomRoot.close();
}
onRejected: {
joinRoomRoot.close();
}
Button {
text: "Join"
enabled: input.text.match("#.+?:.{3,}")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
}
Button {
text: "Cancel"
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
}
}
}

View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1
import QtQuick 2.15
import QtQuick.Controls 2.15
import im.nheko 1.0
MessageDialog {
id: leaveRoomRoot
required property string roomId
title: qsTr("Leave room")
text: qsTr("Are you sure you want to leave?")
modality: Qt.ApplicationModal
buttons: Dialog.Ok | Dialog.Cancel
onAccepted: Rooms.leave(roomId)
}

View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1
import QtQuick 2.15
import QtQuick.Controls 2.15
import im.nheko 1.0
MessageDialog {
id: logoutRoot
title: qsTr("Log out")
text: CallManager.isOnCall ? qsTr("A call is in progress. Log out?") : qsTr("Are you sure you want to log out?")
modality: Qt.WindowModal
flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
buttons: Dialog.Ok | Dialog.Cancel
onAccepted: Nheko.logout()
}

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ ApplicationWindow {
width: 420 width: 420
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.window color: Nheko.colors.window
flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(rawMessageRoot) Component.onCompleted: Nheko.reparent(rawMessageRoot)
Shortcut { Shortcut {

View File

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
@ -19,7 +20,7 @@ ApplicationWindow {
minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.window color: Nheko.colors.window
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(readReceiptsRoot) Component.onCompleted: Nheko.reparent(readReceiptsRoot)
Shortcut { Shortcut {

View File

@ -0,0 +1,217 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import "../ui"
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import im.nheko 1.0
ApplicationWindow {
id: roomDirectoryWindow
property RoomDirectoryModel publicRooms
visible: true
minimumWidth: 650
minimumHeight: 420
palette: Nheko.colors
color: Nheko.colors.window
modality: Qt.WindowModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(roomDirectoryWindow)
title: qsTr("Explore Public Rooms")
Shortcut {
sequence: StandardKey.Cancel
onActivated: roomDirectoryWindow.close()
}
ListView {
id: roomDirView
anchors.fill: parent
model: publicRooms
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
delegate: Rectangle {
id: roomDirDelegate
property color background: Nheko.colors.window
property color importantText: Nheko.colors.text
property color unimportantText: Nheko.colors.buttonText
property int avatarSize: fontMetrics.lineSpacing * 4
color: background
height: avatarSize + Nheko.paddingLarge
width: ListView.view.width
RowLayout {
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingLarge
implicitHeight: textContent.height
Avatar {
id: roomAvatar
Layout.alignment: Qt.AlignVCenter
width: avatarSize
height: avatarSize
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: model.roomid
displayName: model.name
}
ColumnLayout {
id: textContent
Layout.alignment: Qt.AlignLeft
width: parent.width - avatar.width
Layout.preferredWidth: parent.width - avatar.width
spacing: Nheko.paddingSmall
ElidedLabel {
Layout.alignment: Qt.AlignBottom
color: roomDirDelegate.importantText
elideWidth: textContent.width - numMembersRectangle.width - buttonRectangle.width
font.pixelSize: fontMetrics.font.pixelSize * 1.1
fullText: model.name
}
RowLayout {
id: roomDescriptionRow
Layout.preferredWidth: parent.width
spacing: Nheko.paddingSmall
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.preferredHeight: fontMetrics.lineSpacing * 4
Label {
id: roomTopic
color: roomDirDelegate.unimportantText
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
font.pixelSize: fontMetrics.font.pixelSize
elide: Text.ElideRight
maximumLineCount: 2
Layout.fillWidth: true
text: model.topic
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
Item {
id: numMembersRectangle
Layout.margins: Nheko.paddingSmall
width: roomCount.width
Label {
id: roomCount
color: roomDirDelegate.unimportantText
anchors.centerIn: parent
font.pixelSize: fontMetrics.font.pixelSize
text: model.numMembers.toString()
}
}
Item {
id: buttonRectangle
Layout.margins: Nheko.paddingSmall
width: joinRoomButton.width
Button {
id: joinRoomButton
visible: model.canJoin
anchors.centerIn: parent
text: "Join"
onClicked: publicRooms.joinRoom(model.index)
}
}
}
}
}
}
footer: Item {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
visible: !publicRooms.reachedEndOfPagination && publicRooms.loadingMoreRooms
// hacky but works
height: loadingSpinner.height + 2 * Nheko.paddingLarge
anchors.margins: Nheko.paddingLarge
Spinner {
id: loadingSpinner
anchors.centerIn: parent
anchors.margins: Nheko.paddingLarge
running: visible
foreground: Nheko.colors.mid
}
}
}
publicRooms: RoomDirectoryModel {
}
header: RowLayout {
id: searchBarLayout
spacing: Nheko.paddingMedium
width: parent.width
implicitHeight: roomSearch.height
MatrixTextField {
id: roomSearch
focus: true
Layout.fillWidth: true
selectByMouse: true
font.pixelSize: fontMetrics.font.pixelSize
padding: Nheko.paddingMedium
color: Nheko.colors.text
placeholderText: qsTr("Search for public rooms")
onTextChanged: searchTimer.restart()
}
MatrixTextField {
id: chooseServer
Layout.minimumWidth: 0.3 * header.width
Layout.maximumWidth: 0.3 * header.width
padding: Nheko.paddingMedium
color: Nheko.colors.text
placeholderText: qsTr("Choose custom homeserver")
onTextChanged: publicRooms.setMatrixServer(text)
}
Timer {
id: searchTimer
interval: 350
onTriggered: roomDirView.model.setSearchTerm(roomSearch.text)
}
}
}

View File

@ -2,7 +2,8 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import "./ui" import ".."
import "../ui"
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
@ -21,7 +22,7 @@ ApplicationWindow {
minimumHeight: 420 minimumHeight: 420
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.window color: Nheko.colors.window
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(roomMembersRoot) Component.onCompleted: Nheko.reparent(roomMembersRoot)
Shortcut { Shortcut {
@ -39,6 +40,7 @@ ApplicationWindow {
width: 130 width: 130
height: width height: width
roomid: members.roomId
displayName: members.roomName displayName: members.roomName
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
url: members.avatarUrl.replace("mxc://", "image://MxcImage/") url: members.avatarUrl.replace("mxc://", "image://MxcImage/")

View File

@ -2,7 +2,8 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import "./ui" import ".."
import "../ui"
import Qt.labs.platform 1.1 as Platform import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
@ -20,7 +21,7 @@ ApplicationWindow {
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.window color: Nheko.colors.window
modality: Qt.NonModal modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(roomSettingsDialog) Component.onCompleted: Nheko.reparent(roomSettingsDialog)
title: qsTr("Room Settings") title: qsTr("Room Settings")
@ -38,6 +39,7 @@ ApplicationWindow {
Avatar { Avatar {
url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/") url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
roomid: roomSettings.roomId
displayName: roomSettings.roomName displayName: roomSettings.roomName
height: 130 height: 130
width: 130 width: 130
@ -186,7 +188,16 @@ ApplicationWindow {
ComboBox { ComboBox {
enabled: roomSettings.canChangeJoinRules enabled: roomSettings.canChangeJoinRules
model: [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")] model: {
let opts = [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")];
if (roomSettings.supportsKnocking)
opts.push(qsTr("By knocking"));
if (roomSettings.supportsRestricted)
opts.push(qsTr("Restricted by membership in other rooms"));
return opts;
}
currentIndex: roomSettings.accessJoinRules currentIndex: roomSettings.accessJoinRules
onActivated: { onActivated: {
roomSettings.changeAccessRules(index); roomSettings.changeAccessRules(index);

View File

@ -0,0 +1,433 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import "../device-verification"
import "../ui"
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
import im.nheko 1.0
ApplicationWindow {
// this does not work in ApplicationWindow, just in Window
//transientParent: Nheko.mainwindow()
id: userProfileDialog
property var profile
height: 650
width: 420
minimumWidth: 150
minimumHeight: 150
palette: Nheko.colors
color: Nheko.colors.window
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(userProfileDialog)
Shortcut {
sequence: StandardKey.Cancel
onActivated: userProfileDialog.close()
}
ListView {
id: devicelist
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
spacing: 8
boundsBehavior: Flickable.StopAtBounds
model: profile.deviceList
anchors.fill: parent
anchors.margins: 10
footerPositioning: ListView.OverlayFooter
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
header: ColumnLayout {
id: contentL
width: devicelist.width
spacing: 10
Avatar {
id: displayAvatar
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
height: 130
width: 130
displayName: profile.displayName
userid: profile.userid
Layout.alignment: Qt.AlignHCenter
onClicked: TimelineManager.openImageOverlay(profile.avatarUrl, "")
ImageButton {
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: profile.isGlobalUserProfile ? qsTr("Change avatar globally.") : qsTr("Change avatar. Will only apply to this room.")
anchors.left: displayAvatar.left
anchors.top: displayAvatar.top
anchors.leftMargin: Nheko.paddingMedium
anchors.topMargin: Nheko.paddingMedium
visible: profile.isSelf
image: ":/icons/icons/ui/edit.png"
onClicked: profile.changeAvatar()
}
}
Spinner {
Layout.alignment: Qt.AlignHCenter
running: profile.isLoading
visible: profile.isLoading
foreground: Nheko.colors.mid
}
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 {
function onDisplayError(errorMessage) {
errorText.text = errorMessage;
errorText.opacity = 1;
hideErrorAnimation.restart();
}
target: profile
}
TextInput {
id: displayUsername
property bool isUsernameEditingAllowed
readOnly: !isUsernameEditingAllowed
text: profile.displayName
font.pixelSize: 20
color: TimelineManager.userColor(profile.userid, Nheko.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: Nheko.paddingSmall
anchors.left: displayUsername.right
anchors.verticalCenter: displayUsername.verticalCenter
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: profile.isGlobalUserProfile ? qsTr("Change display name globally.") : qsTr("Change display name. Will only apply to this room.")
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 {
text: profile.userid
Layout.alignment: Qt.AlignHCenter
}
RowLayout {
visible: !profile.isGlobalUserProfile
Layout.alignment: Qt.AlignHCenter
spacing: Nheko.paddingSmall
MatrixText {
id: displayRoomname
text: qsTr("Room: %1").arg(profile.room ? profile.room.roomName : "")
ToolTip.text: qsTr("This is a room-specific profile. The user's name and avatar may be different from their global versions.")
ToolTip.visible: ma.hovered
HoverHandler {
id: ma
}
}
ImageButton {
image: ":/icons/icons/ui/world.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Open the global profile for this user.")
onClicked: profile.openGlobalProfile()
}
}
Button {
id: verifyUserButton
text: qsTr("Verify")
Layout.alignment: Qt.AlignHCenter
enabled: profile.userVerified != Crypto.Verified
visible: profile.userVerified != Crypto.Verified && !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" : Nheko.colors.buttonText)
visible: profile.userVerified != Crypto.Unverified
Layout.alignment: Qt.AlignHCenter
}
RowLayout {
// ImageButton{
// image:":/icons/icons/ui/volume-off-indicator.png"
// Layout.margins: {
// left: 5
// right: 5
// }
// ToolTip.visible: hovered
// ToolTip.text: qsTr("Ignore messages from this user.")
// onClicked : {
// profile.ignoreUser()
// }
// }
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 10
spacing: Nheko.paddingSmall
ImageButton {
image: ":/icons/icons/ui/black-bubble-speech.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Start a private chat.")
onClicked: profile.startChat()
}
ImageButton {
image: ":/icons/icons/ui/round-remove-button.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Kick the user.")
onClicked: profile.kickUser()
visible: !profile.isGlobalUserProfile && profile.room.permissions.canKick()
}
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.isGlobalUserProfile && profile.room.permissions.canBan()
}
ImageButton {
image: ":/icons/icons/ui/refresh.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Refresh device list.")
onClicked: profile.refreshDevices()
}
}
}
delegate: RowLayout {
required property int verificationStatus
required property string deviceId
required property string deviceName
required property string lastIp
required property var lastTs
width: devicelist.width
spacing: 4
ColumnLayout {
spacing: 0
RowLayout {
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
elide: Text.ElideRight
font.bold: true
color: Nheko.colors.text
text: deviceId
}
Image {
Layout.preferredHeight: 16
Layout.preferredWidth: 16
visible: profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE
source: {
switch (verificationStatus) {
case VerificationStatus.VERIFIED:
return "image://colorimage/:/icons/icons/ui/lock.png?green";
case VerificationStatus.UNVERIFIED:
return "image://colorimage/:/icons/icons/ui/unlock.png?yellow";
case VerificationStatus.SELF:
return "image://colorimage/:/icons/icons/ui/checkmark.png?green";
default:
return "image://colorimage/:/icons/icons/ui/unlock.png?red";
}
}
}
ImageButton {
Layout.alignment: Qt.AlignTop
image: ":/icons/icons/ui/power-button-off.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Sign out this device.")
onClicked: profile.signOutDevice(deviceId)
visible: profile.isSelf
}
}
RowLayout {
id: deviceNameRow
property bool isEditingAllowed
TextInput {
id: deviceNameField
readOnly: !deviceNameRow.isEditingAllowed
text: deviceName
color: Nheko.colors.text
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
selectByMouse: true
onAccepted: {
profile.changeDeviceName(deviceId, deviceNameField.text);
deviceNameRow.isEditingAllowed = false;
}
}
ImageButton {
visible: profile.isSelf
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Change device name.")
image: deviceNameRow.isEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
onClicked: {
if (deviceNameRow.isEditingAllowed) {
profile.changeDeviceName(deviceId, deviceNameField.text);
deviceNameRow.isEditingAllowed = false;
} else {
deviceNameRow.isEditingAllowed = true;
deviceNameField.focus = true;
deviceNameField.selectAll();
}
}
}
}
Text {
visible: profile.isSelf
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
elide: Text.ElideRight
color: Nheko.colors.text
text: qsTr("Last seen %1 from %2").arg(new Date(lastTs).toLocaleString(Locale.ShortFormat)).arg(lastIp ? lastIp : "???")
}
}
Image {
Layout.preferredHeight: 16
Layout.preferredWidth: 16
visible: !profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE
source: {
switch (verificationStatus) {
case VerificationStatus.VERIFIED:
return "image://colorimage/:/icons/icons/ui/lock.png?green";
case VerificationStatus.UNVERIFIED:
return "image://colorimage/:/icons/icons/ui/unlock.png?yellow";
case VerificationStatus.SELF:
return "image://colorimage/:/icons/icons/ui/checkmark.png?green";
default:
return "image://colorimage/:/icons/icons/ui/unlock.png?red";
}
}
}
Button {
id: verifyButton
visible: verificationStatus == VerificationStatus.UNVERIFIED && (profile.isSelf || !profile.userVerificationEnabled)
text: (verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
onClicked: {
if (verificationStatus == VerificationStatus.VERIFIED)
profile.unverify(deviceId);
else
profile.verify(deviceId);
}
}
}
footer: DialogButtonBox {
z: 2
width: devicelist.width
alignment: Qt.AlignRight
standardButtons: DialogButtonBox.Ok
onAccepted: userProfileDialog.close()
background: Rectangle {
anchors.fill: parent
color: Nheko.colors.window
}
}
}
}

View File

@ -72,7 +72,8 @@ Menu {
onVisibleChanged: { onVisibleChanged: {
if (visible) if (visible)
forceActiveFocus(); forceActiveFocus();
else
clear();
} }
Timer { Timer {

View File

@ -34,14 +34,15 @@ Rectangle {
width: Nheko.avatarSize width: Nheko.avatarSize
height: Nheko.avatarSize height: Nheko.avatarSize
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: CallManager.callParty userid: CallManager.callParty
displayName: CallManager.callPartyDisplayName
onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
} }
Label { Label {
Layout.leftMargin: 8 Layout.leftMargin: 8
font.pointSize: fontMetrics.font.pointSize * 1.1 font.pointSize: fontMetrics.font.pointSize * 1.1
text: CallManager.callParty text: CallManager.callPartyDisplayName
color: "#000000" color: "#000000"
} }

View File

@ -40,7 +40,7 @@ Popup {
Label { Label {
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
Layout.topMargin: msgView.height / 25 Layout.topMargin: msgView.height / 25
text: CallManager.callParty text: CallManager.callPartyDisplayName
font.pointSize: fontMetrics.font.pointSize * 2 font.pointSize: fontMetrics.font.pointSize * 2
color: Nheko.colors.windowText color: Nheko.colors.windowText
} }
@ -50,7 +50,8 @@ Popup {
width: msgView.height / 5 width: msgView.height / 5
height: msgView.height / 5 height: msgView.height / 5
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: CallManager.callParty userid: CallManager.callParty
displayName: CallManager.callPartyDisplayName
} }
ColumnLayout { ColumnLayout {

View File

@ -41,14 +41,15 @@ Rectangle {
width: Nheko.avatarSize width: Nheko.avatarSize
height: Nheko.avatarSize height: Nheko.avatarSize
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: CallManager.callParty userid: CallManager.callParty
displayName: CallManager.callPartyDisplayName
onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
} }
Label { Label {
Layout.leftMargin: 8 Layout.leftMargin: 8
font.pointSize: fontMetrics.font.pointSize * 1.1 font.pointSize: fontMetrics.font.pointSize * 1.1
text: CallManager.callParty text: CallManager.callPartyDisplayName
color: "#000000" color: "#000000"
} }

View File

@ -79,6 +79,7 @@ Popup {
height: Nheko.avatarSize height: Nheko.avatarSize
url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: room.roomName displayName: room.roomName
roomid: room.roomid
onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
} }

View File

@ -73,6 +73,7 @@
<file>icons/ui/screen-share.png</file> <file>icons/ui/screen-share.png</file>
<file>icons/ui/toggle-camera-view.png</file> <file>icons/ui/toggle-camera-view.png</file>
<file>icons/ui/video-call.png</file> <file>icons/ui/video-call.png</file>
<file>icons/ui/refresh.png</file>
<file>icons/emoji-categories/people.png</file> <file>icons/emoji-categories/people.png</file>
<file>icons/emoji-categories/people@2x.png</file> <file>icons/emoji-categories/people@2x.png</file>
<file>icons/emoji-categories/nature.png</file> <file>icons/emoji-categories/nature.png</file>
@ -138,31 +139,47 @@
<file>qml/TopBar.qml</file> <file>qml/TopBar.qml</file>
<file>qml/QuickSwitcher.qml</file> <file>qml/QuickSwitcher.qml</file>
<file>qml/ForwardCompleter.qml</file> <file>qml/ForwardCompleter.qml</file>
<file>qml/SelfVerificationCheck.qml</file>
<file>qml/TypingIndicator.qml</file> <file>qml/TypingIndicator.qml</file>
<file>qml/RoomSettings.qml</file> <file>qml/NotificationWarning.qml</file>
<file>qml/emoji/EmojiPicker.qml</file> <file>qml/components/AdaptiveLayout.qml</file>
<file>qml/emoji/StickerPicker.qml</file> <file>qml/components/AdaptiveLayoutElement.qml</file>
<file>qml/UserProfile.qml</file> <file>qml/components/AvatarListTile.qml</file>
<file>qml/delegates/MessageDelegate.qml</file> <file>qml/components/FlatButton.qml</file>
<file>qml/components/MainWindowDialog.qml</file>
<file>qml/delegates/Encrypted.qml</file> <file>qml/delegates/Encrypted.qml</file>
<file>qml/delegates/FileMessage.qml</file> <file>qml/delegates/FileMessage.qml</file>
<file>qml/delegates/ImageMessage.qml</file> <file>qml/delegates/ImageMessage.qml</file>
<file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/NoticeMessage.qml</file> <file>qml/delegates/NoticeMessage.qml</file>
<file>qml/delegates/Pill.qml</file> <file>qml/delegates/Pill.qml</file>
<file>qml/delegates/Placeholder.qml</file> <file>qml/delegates/Placeholder.qml</file>
<file>qml/delegates/PlayableMediaMessage.qml</file> <file>qml/delegates/PlayableMediaMessage.qml</file>
<file>qml/delegates/Reply.qml</file> <file>qml/delegates/Reply.qml</file>
<file>qml/delegates/TextMessage.qml</file> <file>qml/delegates/TextMessage.qml</file>
<file>qml/device-verification/Waiting.qml</file>
<file>qml/device-verification/DeviceVerification.qml</file> <file>qml/device-verification/DeviceVerification.qml</file>
<file>qml/device-verification/DigitVerification.qml</file> <file>qml/device-verification/DigitVerification.qml</file>
<file>qml/device-verification/EmojiVerification.qml</file> <file>qml/device-verification/EmojiVerification.qml</file>
<file>qml/device-verification/NewVerificationRequest.qml</file>
<file>qml/device-verification/Failed.qml</file> <file>qml/device-verification/Failed.qml</file>
<file>qml/device-verification/NewVerificationRequest.qml</file>
<file>qml/device-verification/Success.qml</file> <file>qml/device-verification/Success.qml</file>
<file>qml/dialogs/InputDialog.qml</file> <file>qml/device-verification/Waiting.qml</file>
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
<file>qml/dialogs/ImagePackEditorDialog.qml</file> <file>qml/dialogs/ImagePackEditorDialog.qml</file>
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
<file>qml/dialogs/PhoneNumberInputDialog.qml</file>
<file>qml/dialogs/InputDialog.qml</file>
<file>qml/dialogs/InviteDialog.qml</file>
<file>qml/dialogs/JoinRoomDialog.qml</file>
<file>qml/dialogs/LeaveRoomDialog.qml</file>
<file>qml/dialogs/LogoutDialog.qml</file>
<file>qml/dialogs/RawMessageDialog.qml</file>
<file>qml/dialogs/ReadReceipts.qml</file>
<file>qml/dialogs/RoomDirectory.qml</file>
<file>qml/dialogs/RoomMembers.qml</file>
<file>qml/dialogs/RoomSettings.qml</file>
<file>qml/dialogs/UserProfile.qml</file>
<file>qml/emoji/EmojiPicker.qml</file>
<file>qml/emoji/StickerPicker.qml</file>
<file>qml/ui/Ripple.qml</file> <file>qml/ui/Ripple.qml</file>
<file>qml/ui/Spinner.qml</file> <file>qml/ui/Spinner.qml</file>
<file>qml/ui/animations/BlinkAnimation.qml</file> <file>qml/ui/animations/BlinkAnimation.qml</file>
@ -174,14 +191,6 @@
<file>qml/voip/PlaceCall.qml</file> <file>qml/voip/PlaceCall.qml</file>
<file>qml/voip/ScreenShare.qml</file> <file>qml/voip/ScreenShare.qml</file>
<file>qml/voip/VideoCall.qml</file> <file>qml/voip/VideoCall.qml</file>
<file>qml/components/AdaptiveLayout.qml</file>
<file>qml/components/AdaptiveLayoutElement.qml</file>
<file>qml/components/AvatarListTile.qml</file>
<file>qml/components/FlatButton.qml</file>
<file>qml/RoomMembers.qml</file>
<file>qml/InviteDialog.qml</file>
<file>qml/ReadReceipts.qml</file>
<file>qml/RawMessageDialog.qml</file>
</qresource> </qresource>
<qresource prefix="/media"> <qresource prefix="/media">
<file>media/ring.ogg</file> <file>media/ring.ogg</file>

View File

@ -54,7 +54,7 @@ if __name__ == '__main__':
} }
current_category = '' current_category = ''
for line in open(filename, 'r'): for line in open(filename, 'r', encoding="utf8"):
if line.startswith('# group:'): if line.startswith('# group:'):
current_category = line.split(':', 1)[1].strip() current_category = line.split(':', 1)[1].strip()

View File

@ -22,45 +22,44 @@ namespace AvatarProvider {
void void
resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback) resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
{ {
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
QPixmap pixmap; QPixmap pixmap;
if (avatarUrl.isEmpty()) { if (avatarUrl.isEmpty()) {
callback(pixmap); callback(pixmap);
return; return;
} }
if (avatar_cache.find(cacheKey, &pixmap)) { if (avatar_cache.find(cacheKey, &pixmap)) {
callback(pixmap); callback(pixmap);
return; return;
} }
MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")), MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
QSize(size, size), QSize(size, size),
[callback, cacheKey, recv = QPointer<QObject>(receiver)]( [callback, cacheKey, recv = QPointer<QObject>(receiver)](
QString, QSize, QImage img, QString) { QString, QSize, QImage img, QString) {
if (!recv) if (!recv)
return; return;
auto proxy = std::make_shared<AvatarProxy>(); auto proxy = std::make_shared<AvatarProxy>();
QObject::connect(proxy.get(), QObject::connect(proxy.get(),
&AvatarProxy::avatarDownloaded, &AvatarProxy::avatarDownloaded,
recv, recv,
[callback, cacheKey](QPixmap pm) { [callback, cacheKey](QPixmap pm) {
if (!pm.isNull()) if (!pm.isNull())
avatar_cache.insert( avatar_cache.insert(cacheKey, pm);
cacheKey, pm); callback(pm);
callback(pm); });
});
if (img.isNull()) { if (img.isNull()) {
emit proxy->avatarDownloaded(QPixmap{}); emit proxy->avatarDownloaded(QPixmap{});
return; return;
} }
auto pm = QPixmap::fromImage(std::move(img)); auto pm = QPixmap::fromImage(std::move(img));
emit proxy->avatarDownloaded(pm); emit proxy->avatarDownloaded(pm);
}); });
} }
void void
@ -70,8 +69,8 @@ resolve(const QString &room_id,
QObject *receiver, QObject *receiver,
AvatarCallback callback) AvatarCallback callback)
{ {
auto avatarUrl = cache::avatarUrl(room_id, user_id); auto avatarUrl = cache::avatarUrl(room_id, user_id);
resolve(std::move(avatarUrl), size, receiver, callback); resolve(std::move(avatarUrl), size, receiver, callback);
} }
} }

View File

@ -12,10 +12,10 @@ using AvatarCallback = std::function<void(QPixmap)>;
class AvatarProxy : public QObject class AvatarProxy : public QObject
{ {
Q_OBJECT Q_OBJECT
signals: signals:
void avatarDownloaded(QPixmap pm); void avatarDownloaded(QPixmap pm);
}; };
namespace AvatarProvider { namespace AvatarProvider {

View File

@ -13,33 +13,33 @@
void void
BlurhashResponse::run() BlurhashResponse::run()
{ {
if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) { if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) {
m_error = QStringLiteral("Blurhash needs size request"); m_error = QStringLiteral("Blurhash needs size request");
emit finished();
return;
}
if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) {
m_image = QImage(m_requestedSize, QImage::Format_RGB32);
m_image.fill(QColor(0, 0, 0));
emit finished();
return;
}
auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(),
m_requestedSize.width(),
m_requestedSize.height());
if (decoded.image.empty()) {
m_error = QStringLiteral("Failed decode!");
emit finished();
return;
}
QImage image(decoded.image.data(),
(int)decoded.width,
(int)decoded.height,
(int)decoded.width * 3,
QImage::Format_RGB888);
m_image = image.copy();
emit finished(); emit finished();
return;
}
if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) {
m_image = QImage(m_requestedSize, QImage::Format_RGB32);
m_image.fill(QColor(0, 0, 0));
emit finished();
return;
}
auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(),
m_requestedSize.width(),
m_requestedSize.height());
if (decoded.image.empty()) {
m_error = QStringLiteral("Failed decode!");
emit finished();
return;
}
QImage image(decoded.image.data(),
(int)decoded.width,
(int)decoded.height,
(int)decoded.width * 3,
QImage::Format_RGB888);
m_image = image.copy();
emit finished();
} }

View File

@ -15,41 +15,41 @@ class BlurhashResponse
, public QRunnable , public QRunnable
{ {
public: public:
BlurhashResponse(const QString &id, const QSize &requestedSize) BlurhashResponse(const QString &id, const QSize &requestedSize)
: m_id(id) : m_id(id)
, m_requestedSize(requestedSize) , m_requestedSize(requestedSize)
{ {
setAutoDelete(false); setAutoDelete(false);
} }
QQuickTextureFactory *textureFactory() const override QQuickTextureFactory *textureFactory() const override
{ {
return QQuickTextureFactory::textureFactoryForImage(m_image); return QQuickTextureFactory::textureFactoryForImage(m_image);
} }
QString errorString() const override { return m_error; } QString errorString() const override { return m_error; }
void run() override; void run() override;
QString m_id, m_error; QString m_id, m_error;
QSize m_requestedSize; QSize m_requestedSize;
QImage m_image; QImage m_image;
}; };
class BlurhashProvider class BlurhashProvider
: public QObject : public QObject
, public QQuickAsyncImageProvider , public QQuickAsyncImageProvider
{ {
Q_OBJECT Q_OBJECT
public slots: public slots:
QQuickImageResponse *requestImageResponse(const QString &id, QQuickImageResponse *requestImageResponse(const QString &id,
const QSize &requestedSize) override const QSize &requestedSize) override
{ {
BlurhashResponse *response = new BlurhashResponse(id, requestedSize); BlurhashResponse *response = new BlurhashResponse(id, requestedSize);
pool.start(response); pool.start(response);
return response; return response;
} }
private: private:
QThreadPool pool; QThreadPool pool;
}; };

File diff suppressed because it is too large Load Diff

View File

@ -83,6 +83,9 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
//! Retrieve member info from a room. //! Retrieve member info from a room.
std::vector<RoomMember> std::vector<RoomMember>
getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30);
//! Retrive member info from an invite.
std::vector<RoomMember>
getMembersFromInvite(const std::string &room_id, std::size_t start_index = 0, std::size_t len = 30);
bool bool
isInitialized(); isInitialized();

View File

@ -19,43 +19,48 @@ Q_NAMESPACE
//! How much a participant is trusted. //! How much a participant is trusted.
enum Trust enum Trust
{ {
Unverified, //! Device unverified or master key changed. Unverified, //! Device unverified or master key changed.
TOFU, //! Device is signed by the sender, but the user is not verified, but they never TOFU, //! Device is signed by the sender, but the user is not verified, but they never
//! changed the master key. //! changed the master key.
Verified, //! User was verified and has crosssigned this device or device is verified. Verified, //! User was verified and has crosssigned this device or device is verified.
}; };
Q_ENUM_NS(Trust) Q_ENUM_NS(Trust)
} }
struct DeviceKeysToMsgIndex struct DeviceKeysToMsgIndex
{ {
// map from device key to message_index // map from device key to message_index
// Using the device id is safe because we check for reuse on device list updates // Using the device id is safe because we check for reuse on device list updates
// Using the device id makes our logic much easier to read. // Using the device id makes our logic much easier to read.
std::map<std::string, uint64_t> deviceids; std::map<std::string, uint64_t> deviceids;
}; };
struct SharedWithUsers struct SharedWithUsers
{ {
// userid to keys // userid to keys
std::map<std::string, DeviceKeysToMsgIndex> keys; std::map<std::string, DeviceKeysToMsgIndex> keys;
}; };
// Extra information associated with an outbound megolm session. // Extra information associated with an outbound megolm session.
struct GroupSessionData struct GroupSessionData
{ {
uint64_t message_index = 0; uint64_t message_index = 0;
uint64_t timestamp = 0; uint64_t timestamp = 0;
std::string sender_claimed_ed25519_key; // If we got the session via key sharing or forwarding, we can usually trust it.
std::vector<std::string> forwarding_curve25519_key_chain; // If it came from asymmetric key backup, it is not trusted.
// TODO(Nico): What about forwards? They might come from key backup?
bool trusted = true;
//! map from index to event_id to check for replay attacks std::string sender_claimed_ed25519_key;
std::map<uint32_t, std::string> indices; std::vector<std::string> forwarding_curve25519_key_chain;
// who has access to this session. //! map from index to event_id to check for replay attacks
// Rotate, when a user leaves the room and share, when a user gets added. std::map<uint32_t, std::string> indices;
SharedWithUsers currently;
// who has access to this session.
// Rotate, when a user leaves the room and share, when a user gets added.
SharedWithUsers currently;
}; };
void void
@ -65,14 +70,14 @@ from_json(const nlohmann::json &obj, GroupSessionData &msg);
struct OutboundGroupSessionDataRef struct OutboundGroupSessionDataRef
{ {
mtx::crypto::OutboundGroupSessionPtr session; mtx::crypto::OutboundGroupSessionPtr session;
GroupSessionData data; GroupSessionData data;
}; };
struct DevicePublicKeys struct DevicePublicKeys
{ {
std::string ed25519; std::string ed25519;
std::string curve25519; std::string curve25519;
}; };
void void
@ -83,12 +88,19 @@ from_json(const nlohmann::json &obj, DevicePublicKeys &msg);
//! Represents a unique megolm session identifier. //! Represents a unique megolm session identifier.
struct MegolmSessionIndex struct MegolmSessionIndex
{ {
//! The room in which this session exists. MegolmSessionIndex() = default;
std::string room_id; MegolmSessionIndex(std::string room_id_, const mtx::events::msg::Encrypted &e)
//! The session_id of the megolm session. : room_id(std::move(room_id_))
std::string session_id; , session_id(e.session_id)
//! The curve25519 public key of the sender. , sender_key(e.sender_key)
std::string sender_key; {}
//! The room in which this session exists.
std::string room_id;
//! The session_id of the megolm session.
std::string session_id;
//! The curve25519 public key of the sender.
std::string sender_key;
}; };
void void
@ -98,8 +110,8 @@ from_json(const nlohmann::json &obj, MegolmSessionIndex &msg);
struct StoredOlmSession struct StoredOlmSession
{ {
std::uint64_t last_message_ts = 0; std::uint64_t last_message_ts = 0;
std::string pickled_session; std::string pickled_session;
}; };
void void
to_json(nlohmann::json &obj, const StoredOlmSession &msg); to_json(nlohmann::json &obj, const StoredOlmSession &msg);
@ -109,43 +121,43 @@ from_json(const nlohmann::json &obj, StoredOlmSession &msg);
//! Verification status of a single user //! Verification status of a single user
struct VerificationStatus struct VerificationStatus
{ {
//! True, if the users master key is verified //! True, if the users master key is verified
crypto::Trust user_verified = crypto::Trust::Unverified; crypto::Trust user_verified = crypto::Trust::Unverified;
//! List of all devices marked as verified //! List of all devices marked as verified
std::set<std::string> verified_devices; std::set<std::string> verified_devices;
//! Map from sender key/curve25519 to trust status //! Map from sender key/curve25519 to trust status
std::map<std::string, crypto::Trust> verified_device_keys; std::map<std::string, crypto::Trust> verified_device_keys;
//! Count of unverified devices //! Count of unverified devices
int unverified_device_count = 0; int unverified_device_count = 0;
// if the keys are not in cache // if the keys are not in cache
bool no_keys = false; bool no_keys = false;
}; };
//! In memory cache of verification status //! In memory cache of verification status
struct VerificationStorage struct VerificationStorage
{ {
//! mapping of user to verification status //! mapping of user to verification status
std::map<std::string, VerificationStatus> status; std::map<std::string, VerificationStatus> status;
std::mutex verification_storage_mtx; std::mutex verification_storage_mtx;
}; };
// this will store the keys of the user with whom a encrypted room is shared with // this will store the keys of the user with whom a encrypted room is shared with
struct UserKeyCache struct UserKeyCache
{ {
//! Device id to device keys //! Device id to device keys
std::map<std::string, mtx::crypto::DeviceKeys> device_keys; std::map<std::string, mtx::crypto::DeviceKeys> device_keys;
//! cross signing keys //! cross signing keys
mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys; mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
//! Sync token when nheko last fetched the keys //! Sync token when nheko last fetched the keys
std::string updated_at; std::string updated_at;
//! Sync token when the keys last changed. updated != last_changed means they are outdated. //! Sync token when the keys last changed. updated != last_changed means they are outdated.
std::string last_changed; std::string last_changed;
//! if the master key has ever changed //! if the master key has ever changed
bool master_key_changed = false; bool master_key_changed = false;
//! Device keys that were already used at least once //! Device keys that were already used at least once
std::set<std::string> seen_device_keys; std::set<std::string> seen_device_keys;
//! Device ids that were already used at least once //! Device ids that were already used at least once
std::set<std::string> seen_device_ids; std::set<std::string> seen_device_ids;
}; };
void void
@ -157,13 +169,26 @@ from_json(const nlohmann::json &j, UserKeyCache &info);
// UserKeyCache stores only keys of users with which encrypted room is shared // UserKeyCache stores only keys of users with which encrypted room is shared
struct VerificationCache struct VerificationCache
{ {
//! list of verified device_ids with device-verification //! list of verified device_ids with device-verification
std::set<std::string> device_verified; std::set<std::string> device_verified;
//! list of devices the user blocks //! list of devices the user blocks
std::set<std::string> device_blocked; std::set<std::string> device_blocked;
}; };
void void
to_json(nlohmann::json &j, const VerificationCache &info); to_json(nlohmann::json &j, const VerificationCache &info);
void void
from_json(const nlohmann::json &j, VerificationCache &info); from_json(const nlohmann::json &j, VerificationCache &info);
struct OnlineBackupVersion
{
//! the version of the online backup currently enabled
std::string version;
//! the algorithm used by the backup
std::string algorithm;
};
void
to_json(nlohmann::json &j, const OnlineBackupVersion &info);
void
from_json(const nlohmann::json &j, OnlineBackupVersion &info);

View File

@ -16,23 +16,23 @@
namespace cache { namespace cache {
enum class CacheVersion : int enum class CacheVersion : int
{ {
Older = -1, Older = -1,
Current = 0, Current = 0,
Newer = 1, Newer = 1,
}; };
} }
struct RoomMember struct RoomMember
{ {
QString user_id; QString user_id;
QString display_name; QString display_name;
}; };
//! Used to uniquely identify a list of read receipts. //! Used to uniquely identify a list of read receipts.
struct ReadReceiptKey struct ReadReceiptKey
{ {
std::string event_id; std::string event_id;
std::string room_id; std::string room_id;
}; };
void void
@ -43,49 +43,49 @@ from_json(const nlohmann::json &j, ReadReceiptKey &key);
struct DescInfo struct DescInfo
{ {
QString event_id; QString event_id;
QString userid; QString userid;
QString body; QString body;
QString descriptiveTime; QString descriptiveTime;
uint64_t timestamp; uint64_t timestamp;
QDateTime datetime; QDateTime datetime;
}; };
inline bool inline bool
operator==(const DescInfo &a, const DescInfo &b) operator==(const DescInfo &a, const DescInfo &b)
{ {
return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) == return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) ==
std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
} }
inline bool inline bool
operator!=(const DescInfo &a, const DescInfo &b) operator!=(const DescInfo &a, const DescInfo &b)
{ {
return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) != return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) !=
std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
} }
//! UI info associated with a room. //! UI info associated with a room.
struct RoomInfo struct RoomInfo
{ {
//! The calculated name of the room. //! The calculated name of the room.
std::string name; std::string name;
//! The topic of the room. //! The topic of the room.
std::string topic; std::string topic;
//! The calculated avatar url of the room. //! The calculated avatar url of the room.
std::string avatar_url; std::string avatar_url;
//! The calculated version of this room set at creation time. //! The calculated version of this room set at creation time.
std::string version; std::string version;
//! Whether or not the room is an invite. //! Whether or not the room is an invite.
bool is_invite = false; bool is_invite = false;
//! Wheter or not the room is a space //! Wheter or not the room is a space
bool is_space = false; bool is_space = false;
//! Total number of members in the room. //! Total number of members in the room.
size_t member_count = 0; size_t member_count = 0;
//! Who can access to the room. //! Who can access to the room.
mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public; mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public;
bool guest_access = false; bool guest_access = false;
//! The list of tags associated with this room //! The list of tags associated with this room
std::vector<std::string> tags; std::vector<std::string> tags;
}; };
void void
@ -93,11 +93,11 @@ to_json(nlohmann::json &j, const RoomInfo &info);
void void
from_json(const nlohmann::json &j, RoomInfo &info); from_json(const nlohmann::json &j, RoomInfo &info);
//! Basic information per member; //! Basic information per member.
struct MemberInfo struct MemberInfo
{ {
std::string name; std::string name;
std::string avatar_url; std::string avatar_url;
}; };
void void
@ -107,13 +107,13 @@ from_json(const nlohmann::json &j, MemberInfo &info);
struct RoomSearchResult struct RoomSearchResult
{ {
std::string room_id; std::string room_id;
RoomInfo info; RoomInfo info;
}; };
struct ImagePackInfo struct ImagePackInfo
{ {
mtx::events::msc2545::ImagePack pack; mtx::events::msc2545::ImagePack pack;
std::string source_room; std::string source_room;
std::string state_key; std::string state_key;
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,396 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <cstring>
#include <optional>
#include <string_view>
#include "CallDevices.h"
#include "ChatPage.h"
#include "Logging.h"
#include "UserSettingsPage.h"
#ifdef GSTREAMER_AVAILABLE
extern "C"
{
#include "gst/gst.h"
}
#endif
CallDevices::CallDevices()
: QObject()
{}
#ifdef GSTREAMER_AVAILABLE
namespace {
struct AudioSource
{
std::string name;
GstDevice *device;
};
struct VideoSource
{
struct Caps
{
std::string resolution;
std::vector<std::string> frameRates;
};
std::string name;
GstDevice *device;
std::vector<Caps> caps;
};
std::vector<AudioSource> audioSources_;
std::vector<VideoSource> videoSources_;
using FrameRate = std::pair<int, int>;
std::optional<FrameRate>
getFrameRate(const GValue *value)
{
if (GST_VALUE_HOLDS_FRACTION(value)) {
gint num = gst_value_get_fraction_numerator(value);
gint den = gst_value_get_fraction_denominator(value);
return FrameRate{num, den};
}
return std::nullopt;
}
void
addFrameRate(std::vector<std::string> &rates, const FrameRate &rate)
{
constexpr double minimumFrameRate = 15.0;
if (static_cast<double>(rate.first) / rate.second >= minimumFrameRate)
rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second));
}
void
setDefaultDevice(bool isVideo)
{
auto settings = ChatPage::instance()->userSettings();
if (isVideo && settings->camera().isEmpty()) {
const VideoSource &camera = videoSources_.front();
settings->setCamera(QString::fromStdString(camera.name));
settings->setCameraResolution(
QString::fromStdString(camera.caps.front().resolution));
settings->setCameraFrameRate(
QString::fromStdString(camera.caps.front().frameRates.front()));
} else if (!isVideo && settings->microphone().isEmpty()) {
settings->setMicrophone(QString::fromStdString(audioSources_.front().name));
}
}
void
addDevice(GstDevice *device)
{
if (!device)
return;
gchar *name = gst_device_get_display_name(device);
gchar *type = gst_device_get_device_class(device);
bool isVideo = !std::strncmp(type, "Video", 5);
g_free(type);
nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name);
if (!isVideo) {
audioSources_.push_back({name, device});
g_free(name);
setDefaultDevice(false);
return;
}
GstCaps *gstcaps = gst_device_get_caps(device);
if (!gstcaps) {
nhlog::ui()->debug("WebRTC: unable to get caps for {}", name);
g_free(name);
return;
}
VideoSource source{name, device, {}};
g_free(name);
guint nCaps = gst_caps_get_size(gstcaps);
for (guint i = 0; i < nCaps; ++i) {
GstStructure *structure = gst_caps_get_structure(gstcaps, i);
const gchar *struct_name = gst_structure_get_name(structure);
if (!std::strcmp(struct_name, "video/x-raw")) {
gint widthpx, heightpx;
if (gst_structure_get(structure,
"width",
G_TYPE_INT,
&widthpx,
"height",
G_TYPE_INT,
&heightpx,
nullptr)) {
VideoSource::Caps caps;
caps.resolution =
std::to_string(widthpx) + "x" + std::to_string(heightpx);
const GValue *value =
gst_structure_get_value(structure, "framerate");
if (auto fr = getFrameRate(value); fr)
addFrameRate(caps.frameRates, *fr);
else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) {
addFrameRate(
caps.frameRates,
*getFrameRate(gst_value_get_fraction_range_min(value)));
addFrameRate(
caps.frameRates,
*getFrameRate(gst_value_get_fraction_range_max(value)));
} else if (GST_VALUE_HOLDS_LIST(value)) {
guint nRates = gst_value_list_get_size(value);
for (guint j = 0; j < nRates; ++j) {
const GValue *rate =
gst_value_list_get_value(value, j);
if (auto frate = getFrameRate(rate); frate)
addFrameRate(caps.frameRates, *frate);
}
}
if (!caps.frameRates.empty())
source.caps.push_back(std::move(caps));
}
}
}
gst_caps_unref(gstcaps);
videoSources_.push_back(std::move(source));
setDefaultDevice(true);
}
template<typename T>
bool
removeDevice(T &sources, GstDevice *device, bool changed)
{
if (auto it = std::find_if(sources.begin(),
sources.end(),
[device](const auto &s) { return s.device == device; });
it != sources.end()) {
nhlog::ui()->debug(std::string("WebRTC: device ") +
(changed ? "changed: " : "removed: ") + "{}",
it->name);
gst_object_unref(device);
sources.erase(it);
return true;
}
return false;
}
void
removeDevice(GstDevice *device, bool changed)
{
if (device) {
if (removeDevice(audioSources_, device, changed) ||
removeDevice(videoSources_, device, changed))
return;
}
}
gboolean
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data G_GNUC_UNUSED)
{
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_DEVICE_ADDED: {
GstDevice *device;
gst_message_parse_device_added(msg, &device);
addDevice(device);
emit CallDevices::instance().devicesChanged();
break;
}
case GST_MESSAGE_DEVICE_REMOVED: {
GstDevice *device;
gst_message_parse_device_removed(msg, &device);
removeDevice(device, false);
emit CallDevices::instance().devicesChanged();
break;
}
case GST_MESSAGE_DEVICE_CHANGED: {
GstDevice *device;
GstDevice *oldDevice;
gst_message_parse_device_changed(msg, &device, &oldDevice);
removeDevice(oldDevice, true);
addDevice(device);
break;
}
default:
break;
}
return TRUE;
}
template<typename T>
std::vector<std::string>
deviceNames(T &sources, const std::string &defaultDevice)
{
std::vector<std::string> ret;
ret.reserve(sources.size());
for (const auto &s : sources)
ret.push_back(s.name);
// move default device to top of the list
if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end())
std::swap(ret.front(), *it);
return ret;
}
std::optional<VideoSource>
getVideoSource(const std::string &cameraName)
{
if (auto it = std::find_if(videoSources_.cbegin(),
videoSources_.cend(),
[&cameraName](const auto &s) { return s.name == cameraName; });
it != videoSources_.cend()) {
return *it;
}
return std::nullopt;
}
std::pair<int, int>
tokenise(std::string_view str, char delim)
{
std::pair<int, int> ret;
ret.first = std::atoi(str.data());
auto pos = str.find_first_of(delim);
ret.second = std::atoi(str.data() + pos + 1);
return ret;
}
}
void
CallDevices::init()
{
static GstDeviceMonitor *monitor = nullptr;
if (!monitor) {
monitor = gst_device_monitor_new();
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps);
gst_caps_unref(caps);
caps = gst_caps_new_empty_simple("video/x-raw");
gst_device_monitor_add_filter(monitor, "Video/Source", caps);
gst_device_monitor_add_filter(monitor, "Video/Duplex", caps);
gst_caps_unref(caps);
GstBus *bus = gst_device_monitor_get_bus(monitor);
gst_bus_add_watch(bus, newBusMessage, nullptr);
gst_object_unref(bus);
if (!gst_device_monitor_start(monitor)) {
nhlog::ui()->error("WebRTC: failed to start device monitor");
return;
}
}
}
bool
CallDevices::haveMic() const
{
return !audioSources_.empty();
}
bool
CallDevices::haveCamera() const
{
return !videoSources_.empty();
}
std::vector<std::string>
CallDevices::names(bool isVideo, const std::string &defaultDevice) const
{
return isVideo ? deviceNames(videoSources_, defaultDevice)
: deviceNames(audioSources_, defaultDevice);
}
std::vector<std::string>
CallDevices::resolutions(const std::string &cameraName) const
{
std::vector<std::string> ret;
if (auto s = getVideoSource(cameraName); s) {
ret.reserve(s->caps.size());
for (const auto &c : s->caps)
ret.push_back(c.resolution);
}
return ret;
}
std::vector<std::string>
CallDevices::frameRates(const std::string &cameraName, const std::string &resolution) const
{
if (auto s = getVideoSource(cameraName); s) {
if (auto it =
std::find_if(s->caps.cbegin(),
s->caps.cend(),
[&](const auto &c) { return c.resolution == resolution; });
it != s->caps.cend())
return it->frameRates;
}
return {};
}
GstDevice *
CallDevices::audioDevice() const
{
std::string name = ChatPage::instance()->userSettings()->microphone().toStdString();
if (auto it = std::find_if(audioSources_.cbegin(),
audioSources_.cend(),
[&name](const auto &s) { return s.name == name; });
it != audioSources_.cend()) {
nhlog::ui()->debug("WebRTC: microphone: {}", name);
return it->device;
} else {
nhlog::ui()->error("WebRTC: unknown microphone: {}", name);
return nullptr;
}
}
GstDevice *
CallDevices::videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const
{
auto settings = ChatPage::instance()->userSettings();
std::string name = settings->camera().toStdString();
if (auto s = getVideoSource(name); s) {
nhlog::ui()->debug("WebRTC: camera: {}", name);
resolution = tokenise(settings->cameraResolution().toStdString(), 'x');
frameRate = tokenise(settings->cameraFrameRate().toStdString(), '/');
nhlog::ui()->debug(
"WebRTC: camera resolution: {}x{}", resolution.first, resolution.second);
nhlog::ui()->debug(
"WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second);
return s->device;
} else {
nhlog::ui()->error("WebRTC: unknown camera: {}", name);
return nullptr;
}
}
#else
bool
CallDevices::haveMic() const
{
return false;
}
bool
CallDevices::haveCamera() const
{
return false;
}
std::vector<std::string>
CallDevices::names(bool, const std::string &) const
{
return {};
}
std::vector<std::string>
CallDevices::resolutions(const std::string &) const
{
return {};
}
std::vector<std::string>
CallDevices::frameRates(const std::string &, const std::string &) const
{
return {};
}
#endif

View File

@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <utility>
#include <vector>
#include <QObject>
typedef struct _GstDevice GstDevice;
class CallDevices : public QObject
{
Q_OBJECT
public:
static CallDevices &instance()
{
static CallDevices instance;
return instance;
}
bool haveMic() const;
bool haveCamera() const;
std::vector<std::string> names(bool isVideo, const std::string &defaultDevice) const;
std::vector<std::string> resolutions(const std::string &cameraName) const;
std::vector<std::string> frameRates(const std::string &cameraName,
const std::string &resolution) const;
signals:
void devicesChanged();
private:
CallDevices();
friend class WebRTCSession;
void init();
GstDevice *audioDevice() const;
GstDevice *videoDevice(std::pair<int, int> &resolution,
std::pair<int, int> &frameRate) const;
public:
CallDevices(CallDevices const &) = delete;
void operator=(CallDevices const &) = delete;
};

View File

@ -1,689 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <QMediaPlaylist>
#include <QUrl>
#include "Cache.h"
#include "CallDevices.h"
#include "CallManager.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "UserSettingsPage.h"
#include "Utils.h"
#include "mtx/responses/turn_server.hpp"
#ifdef XCB_AVAILABLE
#include <xcb/xcb.h>
#include <xcb/xcb_ewmh.h>
#endif
#ifdef GSTREAMER_AVAILABLE
extern "C"
{
#include "gst/gst.h"
}
#endif
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
Q_DECLARE_METATYPE(mtx::responses::TurnServer)
using namespace mtx::events;
using namespace mtx::events::msg;
using webrtc::CallType;
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer);
}
CallManager::CallManager(QObject *parent)
: QObject(parent)
, session_(WebRTCSession::instance())
, turnServerTimer_(this)
{
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
qRegisterMetaType<mtx::responses::TurnServer>();
connect(
&session_,
&WebRTCSession::offerCreated,
this,
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
emit newMessage(roomid_, CallInvite{callid_, sdp, "0", timeoutms_});
emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
std::string callid(callid_);
QTimer::singleShot(timeoutms_, this, [this, callid]() {
if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) {
hangUp(CallHangUp::Reason::InviteTimeOut);
emit ChatPage::instance()->showNotification(
"The remote side failed to pick up.");
}
});
});
connect(
&session_,
&WebRTCSession::answerCreated,
this,
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
emit newMessage(roomid_, CallAnswer{callid_, sdp, "0"});
emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
});
connect(&session_,
&WebRTCSession::newICECandidate,
this,
[this](const CallCandidates::Candidate &candidate) {
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, "0"});
});
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
connect(this,
&CallManager::turnServerRetrieved,
this,
[this](const mtx::responses::TurnServer &res) {
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
nhlog::net()->info("username: {}", res.username);
nhlog::net()->info("ttl: {} seconds", res.ttl);
for (const auto &u : res.uris)
nhlog::net()->info("uri: {}", u);
// Request new credentials close to expiry
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
turnURIs_ = getTurnURIs(res);
uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
if (res.ttl < 3600)
nhlog::net()->warn("Setting ttl to 1 hour");
turnServerTimer_.setInterval(ttl * 1000 * 0.9);
});
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
switch (state) {
case webrtc::State::DISCONNECTED:
playRingtone(QUrl("qrc:/media/media/callend.ogg"), false);
clear();
break;
case webrtc::State::ICEFAILED: {
QString error("Call connection failed.");
if (turnURIs_.empty())
error += " Your homeserver has no configured TURN server.";
emit ChatPage::instance()->showNotification(error);
hangUp(CallHangUp::Reason::ICEFailed);
break;
}
default:
break;
}
emit newCallState();
});
connect(&CallDevices::instance(),
&CallDevices::devicesChanged,
this,
&CallManager::devicesChanged);
connect(&player_,
&QMediaPlayer::mediaStatusChanged,
this,
[this](QMediaPlayer::MediaStatus status) {
if (status == QMediaPlayer::LoadedMedia)
player_.play();
});
connect(&player_,
QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error),
[this](QMediaPlayer::Error error) {
stopRingtone();
switch (error) {
case QMediaPlayer::FormatError:
case QMediaPlayer::ResourceError:
nhlog::ui()->error("WebRTC: valid ringtone file not found");
break;
case QMediaPlayer::AccessDeniedError:
nhlog::ui()->error("WebRTC: access to ringtone file denied");
break;
default:
nhlog::ui()->error("WebRTC: unable to play ringtone");
break;
}
});
}
void
CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
{
if (isOnCall())
return;
if (callType == CallType::SCREEN) {
if (!screenShareSupported())
return;
if (windows_.empty() || windowIndex >= windows_.size()) {
nhlog::ui()->error("WebRTC: window index out of range");
return;
}
}
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
if (roomInfo.member_count != 2) {
emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms.");
return;
}
std::string errorMessage;
if (!session_.havePlugins(false, &errorMessage) ||
((callType == CallType::VIDEO || callType == CallType::SCREEN) &&
!session_.havePlugins(true, &errorMessage))) {
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
return;
}
callType_ = callType;
roomid_ = roomid;
session_.setTurnServers(turnURIs_);
generateCallID();
std::string strCallType = callType_ == CallType::VOICE
? "voice"
: (callType_ == CallType::VIDEO ? "video" : "screen");
nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType);
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
const RoomMember &callee =
members.front().user_id == utils::localUser() ? members.back() : members.front();
callParty_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
emit newInviteState();
playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true);
if (!session_.createOffer(
callType, callType == CallType::SCREEN ? windows_[windowIndex].second : 0)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
endCall();
}
}
namespace {
std::string
callHangUpReasonString(CallHangUp::Reason reason)
{
switch (reason) {
case CallHangUp::Reason::ICEFailed:
return "ICE failed";
case CallHangUp::Reason::InviteTimeOut:
return "Invite time out";
default:
return "User";
}
}
}
void
CallManager::hangUp(CallHangUp::Reason reason)
{
if (!callid_.empty()) {
nhlog::ui()->debug(
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
emit newMessage(roomid_, CallHangUp{callid_, "0", reason});
endCall();
}
}
void
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
{
#ifdef GSTREAMER_AVAILABLE
if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) ||
handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event))
return;
#else
(void)event;
#endif
}
template<typename T>
bool
CallManager::handleEvent(const mtx::events::collections::TimelineEvents &event)
{
if (std::holds_alternative<RoomEvent<T>>(event)) {
handleEvent(std::get<RoomEvent<T>>(event));
return true;
}
return false;
}
void
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
{
const char video[] = "m=video";
const std::string &sdp = callInviteEvent.content.sdp;
bool isVideo = std::search(sdp.cbegin(),
sdp.cend(),
std::cbegin(video),
std::cend(video) - 1,
[](unsigned char c1, unsigned char c2) {
return std::tolower(c1) == std::tolower(c2);
}) != sdp.cend();
nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
callInviteEvent.content.call_id,
(isVideo ? "video" : "voice"),
callInviteEvent.sender);
if (callInviteEvent.content.call_id.empty())
return;
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
if (isOnCall() || roomInfo.member_count != 2) {
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
"0",
CallHangUp::Reason::InviteTimeOut});
return;
}
const QString &ringtone = ChatPage::instance()->userSettings()->ringtone();
if (ringtone != "Mute")
playRingtone(ringtone == "Default" ? QUrl("qrc:/media/media/ring.ogg")
: QUrl::fromLocalFile(ringtone),
true);
roomid_ = QString::fromStdString(callInviteEvent.room_id);
callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear();
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
const RoomMember &caller =
members.front().user_id == utils::localUser() ? members.back() : members.front();
callParty_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
haveCallInvite_ = true;
callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
inviteSDP_ = callInviteEvent.content.sdp;
emit newInviteState();
}
void
CallManager::acceptInvite()
{
if (!haveCallInvite_)
return;
stopRingtone();
std::string errorMessage;
if (!session_.havePlugins(false, &errorMessage) ||
(callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) {
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
hangUp();
return;
}
session_.setTurnServers(turnURIs_);
if (!session_.acceptOffer(inviteSDP_)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
return;
}
session_.acceptICECandidates(remoteICECandidates_);
remoteICECandidates_.clear();
haveCallInvite_ = false;
emit newInviteState();
}
void
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
{
if (callCandidatesEvent.sender == utils::localUser().toStdString())
return;
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
callCandidatesEvent.content.call_id,
callCandidatesEvent.sender);
if (callid_ == callCandidatesEvent.content.call_id) {
if (isOnCall())
session_.acceptICECandidates(callCandidatesEvent.content.candidates);
else {
// CallInvite has been received and we're awaiting localUser to accept or
// reject the call
for (const auto &c : callCandidatesEvent.content.candidates)
remoteICECandidates_.push_back(c);
}
}
}
void
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
{
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
callAnswerEvent.content.call_id,
callAnswerEvent.sender);
if (callAnswerEvent.sender == utils::localUser().toStdString() &&
callid_ == callAnswerEvent.content.call_id) {
if (!isOnCall()) {
emit ChatPage::instance()->showNotification(
"Call answered on another device.");
stopRingtone();
haveCallInvite_ = false;
emit newInviteState();
}
return;
}
if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
stopRingtone();
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
}
}
}
void
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
{
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
callHangUpEvent.content.call_id,
callHangUpReasonString(callHangUpEvent.content.reason),
callHangUpEvent.sender);
if (callid_ == callHangUpEvent.content.call_id)
endCall();
}
void
CallManager::toggleMicMute()
{
session_.toggleMicMute();
emit micMuteChanged();
}
bool
CallManager::callsSupported()
{
#ifdef GSTREAMER_AVAILABLE
return true;
#else
return false;
#endif
}
bool
CallManager::screenShareSupported()
{
return std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY");
}
QStringList
CallManager::devices(bool isVideo) const
{
QStringList ret;
const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera()
: ChatPage::instance()->userSettings()->microphone();
std::vector<std::string> devices =
CallDevices::instance().names(isVideo, defaultDevice.toStdString());
ret.reserve(devices.size());
std::transform(devices.cbegin(),
devices.cend(),
std::back_inserter(ret),
[](const auto &d) { return QString::fromStdString(d); });
return ret;
}
void
CallManager::generateCallID()
{
using namespace std::chrono;
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
callid_ = "c" + std::to_string(ms);
}
void
CallManager::clear()
{
roomid_.clear();
callParty_.clear();
callPartyAvatarUrl_.clear();
callid_.clear();
callType_ = CallType::VOICE;
haveCallInvite_ = false;
emit newInviteState();
inviteSDP_.clear();
remoteICECandidates_.clear();
}
void
CallManager::endCall()
{
stopRingtone();
session_.end();
clear();
}
void
CallManager::refreshTurnServer()
{
turnURIs_.clear();
turnServerTimer_.start(2000);
}
void
CallManager::retrieveTurnServer()
{
http::client()->get_turn_server(
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
if (err) {
turnServerTimer_.setInterval(5000);
return;
}
emit turnServerRetrieved(res);
});
}
void
CallManager::playRingtone(const QUrl &ringtone, bool repeat)
{
static QMediaPlaylist playlist;
playlist.clear();
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
: QMediaPlaylist::CurrentItemOnce);
playlist.addMedia(ringtone);
player_.setVolume(100);
player_.setPlaylist(&playlist);
}
void
CallManager::stopRingtone()
{
player_.setPlaylist(nullptr);
}
QStringList
CallManager::windowList()
{
windows_.clear();
windows_.push_back({tr("Entire screen"), 0});
#ifdef XCB_AVAILABLE
std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection(
xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); });
if (xcb_connection_has_error(connection.get())) {
nhlog::ui()->error("Failed to connect to X server");
return {};
}
xcb_ewmh_connection_t ewmh;
if (!xcb_ewmh_init_atoms_replies(
&ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) {
nhlog::ui()->error("Failed to connect to EWMH server");
return {};
}
std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>>
ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); });
for (int i = 0; i < ewmh.nb_screens; i++) {
xcb_ewmh_get_windows_reply_t clients;
if (!xcb_ewmh_get_client_list_reply(
&ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) {
nhlog::ui()->error("Failed to request window list");
return {};
}
for (uint32_t w = 0; w < clients.windows_len; w++) {
xcb_window_t window = clients.windows[w];
std::string name;
xcb_ewmh_get_utf8_strings_reply_t data;
auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) {
std::string name(r->strings, r->strings_len);
xcb_ewmh_get_utf8_strings_reply_wipe(r);
return name;
};
xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window);
if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr))
name = getName(&data);
cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window);
if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr))
name = getName(&data);
windows_.push_back({QString::fromStdString(name), window});
}
xcb_ewmh_get_windows_reply_wipe(&clients);
}
#endif
QStringList ret;
ret.reserve(windows_.size());
for (const auto &w : windows_)
ret.append(w.first);
return ret;
}
#ifdef GSTREAMER_AVAILABLE
namespace {
GstElement *pipe_ = nullptr;
unsigned int busWatchId_ = 0;
gboolean
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer G_GNUC_UNUSED)
{
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
if (pipe_) {
gst_element_set_state(GST_ELEMENT(pipe_), GST_STATE_NULL);
gst_object_unref(pipe_);
pipe_ = nullptr;
}
if (busWatchId_) {
g_source_remove(busWatchId_);
busWatchId_ = 0;
}
break;
default:
break;
}
return TRUE;
}
}
#endif
void
CallManager::previewWindow(unsigned int index) const
{
#ifdef GSTREAMER_AVAILABLE
if (windows_.empty() || index >= windows_.size() || !gst_is_initialized())
return;
GstElement *ximagesrc = gst_element_factory_make("ximagesrc", nullptr);
if (!ximagesrc) {
nhlog::ui()->error("Failed to create ximagesrc");
return;
}
GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
GstElement *videoscale = gst_element_factory_make("videoscale", nullptr);
GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr);
GstElement *ximagesink = gst_element_factory_make("ximagesink", nullptr);
g_object_set(ximagesrc, "use-damage", FALSE, nullptr);
g_object_set(ximagesrc, "show-pointer", FALSE, nullptr);
g_object_set(ximagesrc, "xid", windows_[index].second, nullptr);
GstCaps *caps = gst_caps_new_simple(
"video/x-raw", "width", G_TYPE_INT, 480, "height", G_TYPE_INT, 360, nullptr);
g_object_set(capsfilter, "caps", caps, nullptr);
gst_caps_unref(caps);
pipe_ = gst_pipeline_new(nullptr);
gst_bin_add_many(
GST_BIN(pipe_), ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr);
if (!gst_element_link_many(
ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr)) {
nhlog::ui()->error("Failed to link preview window elements");
gst_object_unref(pipe_);
pipe_ = nullptr;
return;
}
if (gst_element_set_state(pipe_, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
nhlog::ui()->error("Unable to start preview pipeline");
gst_object_unref(pipe_);
pipe_ = nullptr;
return;
}
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
busWatchId_ = gst_bus_add_watch(bus, newBusMessage, nullptr);
gst_object_unref(bus);
#else
(void)index;
#endif
}
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer)
{
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
// where username and password are percent-encoded
std::vector<std::string> ret;
for (const auto &uri : turnServer.uris) {
if (auto c = uri.find(':'); c == std::string::npos) {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
} else {
std::string scheme = std::string(uri, 0, c);
if (scheme != "turn" && scheme != "turns") {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
}
QString encodedUri =
QString::fromStdString(scheme) + "://" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) +
":" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) +
"@" + QString::fromStdString(std::string(uri, ++c));
ret.push_back(encodedUri.toStdString());
}
}
return ret;
}
}

View File

@ -1,115 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <vector>
#include <QMediaPlayer>
#include <QObject>
#include <QString>
#include <QTimer>
#include "CallDevices.h"
#include "WebRTCSession.h"
#include "mtx/events/collections.hpp"
#include "mtx/events/voip.hpp"
namespace mtx::responses {
struct TurnServer;
}
class QStringList;
class QUrl;
class CallManager : public QObject
{
Q_OBJECT
Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState)
Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState)
Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState)
Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState)
Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState)
Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
Q_PROPERTY(bool haveLocalPiP READ haveLocalPiP NOTIFY newCallState)
Q_PROPERTY(QStringList mics READ mics NOTIFY devicesChanged)
Q_PROPERTY(QStringList cameras READ cameras NOTIFY devicesChanged)
Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
Q_PROPERTY(bool screenShareSupported READ screenShareSupported CONSTANT)
public:
CallManager(QObject *);
bool haveCallInvite() const { return haveCallInvite_; }
bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; }
webrtc::CallType callType() const { return callType_; }
webrtc::State callState() const { return session_.state(); }
QString callParty() const { return callParty_; }
QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
bool isMicMuted() const { return session_.isMicMuted(); }
bool haveLocalPiP() const { return session_.haveLocalPiP(); }
QStringList mics() const { return devices(false); }
QStringList cameras() const { return devices(true); }
void refreshTurnServer();
static bool callsSupported();
static bool screenShareSupported();
public slots:
void sendInvite(const QString &roomid, webrtc::CallType, unsigned int windowIndex = 0);
void syncEvent(const mtx::events::collections::TimelineEvents &event);
void toggleMicMute();
void toggleLocalPiP() { session_.toggleLocalPiP(); }
void acceptInvite();
void hangUp(
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
QStringList windowList();
void previewWindow(unsigned int windowIndex) const;
signals:
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
void newInviteState();
void newCallState();
void micMuteChanged();
void devicesChanged();
void turnServerRetrieved(const mtx::responses::TurnServer &);
private slots:
void retrieveTurnServer();
private:
WebRTCSession &session_;
QString roomid_;
QString callParty_;
QString callPartyAvatarUrl_;
std::string callid_;
const uint32_t timeoutms_ = 120000;
webrtc::CallType callType_ = webrtc::CallType::VOICE;
bool haveCallInvite_ = false;
std::string inviteSDP_;
std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_;
std::vector<std::string> turnURIs_;
QTimer turnServerTimer_;
QMediaPlayer player_;
std::vector<std::pair<QString, uint32_t>> windows_;
template<typename T>
bool handleEvent(const mtx::events::collections::TimelineEvents &event);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo);
void generateCallID();
QStringList devices(bool isVideo) const;
void clear();
void endCall();
void playRingtone(const QUrl &ringtone, bool repeat);
void stopRingtone();
};

File diff suppressed because it is too large Load Diff

View File

@ -52,183 +52,181 @@ using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2E
class ChatPage : public QWidget class ChatPage : public QWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr);
// Initialize all the components of the UI. // Initialize all the components of the UI.
void bootstrap(QString userid, QString homeserver, QString token); void bootstrap(QString userid, QString homeserver, QString token);
static ChatPage *instance() { return instance_; } static ChatPage *instance() { return instance_; }
QSharedPointer<UserSettings> userSettings() { return userSettings_; } QSharedPointer<UserSettings> userSettings() { return userSettings_; }
CallManager *callManager() { return callManager_; } CallManager *callManager() { return callManager_; }
TimelineViewManager *timelineManager() { return view_manager_; } TimelineViewManager *timelineManager() { return view_manager_; }
void deleteConfigs(); void deleteConfigs();
void initiateLogout(); void initiateLogout();
QString status() const; QString status() const;
void setStatus(const QString &status); void setStatus(const QString &status);
mtx::presence::PresenceState currentPresence() const; mtx::presence::PresenceState currentPresence() const;
// TODO(Nico): Get rid of this! // TODO(Nico): Get rid of this!
QString currentRoom() const; QString currentRoom() const;
public slots: public slots:
void handleMatrixUri(const QByteArray &uri); bool handleMatrixUri(const QByteArray &uri);
void handleMatrixUri(const QUrl &uri); bool handleMatrixUri(const QUrl &uri);
void startChat(QString userid); void startChat(QString userid);
void leaveRoom(const QString &room_id); void leaveRoom(const QString &room_id);
void createRoom(const mtx::requests::CreateRoom &req); void createRoom(const mtx::requests::CreateRoom &req);
void joinRoom(const QString &room); void joinRoom(const QString &room);
void joinRoomVia(const std::string &room_id, void joinRoomVia(const std::string &room_id,
const std::vector<std::string> &via, const std::vector<std::string> &via,
bool promptForConfirmation = true); bool promptForConfirmation = true);
void inviteUser(QString userid, QString reason); void inviteUser(QString userid, QString reason);
void kickUser(QString userid, QString reason); void kickUser(QString userid, QString reason);
void banUser(QString userid, QString reason); void banUser(QString userid, QString reason);
void unbanUser(QString userid, QString reason); void unbanUser(QString userid, QString reason);
void receivedSessionKey(const std::string &room_id, const std::string &session_id); void receivedSessionKey(const std::string &room_id, const std::string &session_id);
void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
const SecretsToDecrypt &secrets); const SecretsToDecrypt &secrets);
signals: signals:
void connectionLost(); void connectionLost();
void connectionRestored(); void connectionRestored();
void notificationsRetrieved(const mtx::responses::Notifications &); void notificationsRetrieved(const mtx::responses::Notifications &);
void highlightedNotifsRetrieved(const mtx::responses::Notifications &, void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos);
const QPoint widgetPos);
void contentLoaded(); void contentLoaded();
void closing(); void closing();
void changeWindowTitle(const int); void changeWindowTitle(const int);
void unreadMessages(int count); void unreadMessages(int count);
void showNotification(const QString &msg); void showNotification(const QString &msg);
void showLoginPage(const QString &msg); void showLoginPage(const QString &msg);
void showUserSettingsPage(); void showUserSettingsPage();
void showOverlayProgressBar(); void showOverlayProgressBar();
void ownProfileOk(); void ownProfileOk();
void setUserDisplayName(const QString &name); void setUserDisplayName(const QString &name);
void setUserAvatar(const QString &avatar); void setUserAvatar(const QString &avatar);
void loggedOut(); void loggedOut();
void trySyncCb(); void trySyncCb();
void tryDelayedSyncCb(); void tryDelayedSyncCb();
void tryInitialSyncCb(); void tryInitialSyncCb();
void newSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token); void newSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token);
void leftRoom(const QString &room_id); void leftRoom(const QString &room_id);
void newRoom(const QString &room_id); void newRoom(const QString &room_id);
void changeToRoom(const QString &room_id);
void initializeViews(const mtx::responses::Rooms &rooms); void initializeViews(const mtx::responses::Rooms &rooms);
void initializeEmptyViews(); void initializeEmptyViews();
void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs);
void syncUI(const mtx::responses::Rooms &rooms); void syncUI(const mtx::responses::Rooms &rooms);
void dropToLoginPageCb(const QString &msg); void dropToLoginPageCb(const QString &msg);
void notifyMessage(const QString &roomid, void notifyMessage(const QString &roomid,
const QString &eventid, const QString &eventid,
const QString &roomname, const QString &roomname,
const QString &sender, const QString &sender,
const QString &message, const QString &message,
const QImage &icon); const QImage &icon);
void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state); void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
void themeChanged(); void themeChanged();
void decryptSidebarChanged(); void decryptSidebarChanged();
void chatFocusChanged(const bool focused); void chatFocusChanged(const bool focused);
//! Signals for device verificaiton //! Signals for device verificaiton
void receivedDeviceVerificationAccept( void receivedDeviceVerificationAccept(const mtx::events::msg::KeyVerificationAccept &message);
const mtx::events::msg::KeyVerificationAccept &message); void receivedDeviceVerificationRequest(const mtx::events::msg::KeyVerificationRequest &message,
void receivedDeviceVerificationRequest( std::string sender);
const mtx::events::msg::KeyVerificationRequest &message, void receivedRoomDeviceVerificationRequest(
std::string sender); const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
void receivedRoomDeviceVerificationRequest( TimelineModel *model);
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, void receivedDeviceVerificationCancel(const mtx::events::msg::KeyVerificationCancel &message);
TimelineModel *model); void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
void receivedDeviceVerificationCancel( void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
const mtx::events::msg::KeyVerificationCancel &message); void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message); std::string sender);
void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message); void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message, void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
std::string sender);
void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
const SecretsToDecrypt &secrets); const SecretsToDecrypt &secrets);
private slots: private slots:
void logout(); void logout();
void removeRoom(const QString &room_id); void removeRoom(const QString &room_id);
void changeRoom(const QString &room_id); void changeRoom(const QString &room_id);
void dropToLoginPage(const QString &msg); void dropToLoginPage(const QString &msg);
void handleSyncResponse(const mtx::responses::Sync &res, void handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token);
const std::string &prev_batch_token);
private: private:
static ChatPage *instance_; static ChatPage *instance_;
void startInitialSync(); void startInitialSync();
void tryInitialSync(); void tryInitialSync();
void trySync(); void trySync();
void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts); void verifyOneTimeKeyCountAfterStartup();
void getProfileInfo(); void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
void getProfileInfo();
void getBackupVersion();
//! Check if the given room is currently open. //! Check if the given room is currently open.
bool isRoomActive(const QString &room_id); bool isRoomActive(const QString &room_id);
using UserID = QString; using UserID = QString;
using Membership = mtx::events::StateEvent<mtx::events::state::Member>; using Membership = mtx::events::StateEvent<mtx::events::state::Member>;
using Memberships = std::map<std::string, Membership>; using Memberships = std::map<std::string, Membership>;
void loadStateFromCache(); void loadStateFromCache();
void resetUI(); void resetUI();
template<class Collection> template<class Collection>
Memberships getMemberships(const std::vector<Collection> &events) const; Memberships getMemberships(const std::vector<Collection> &events) const;
//! Send desktop notification for the received messages. //! Send desktop notification for the received messages.
void sendNotifications(const mtx::responses::Notifications &); void sendNotifications(const mtx::responses::Notifications &);
template<typename T> template<typename T>
void connectCallMessage(); void connectCallMessage();
QHBoxLayout *topLayout_; QHBoxLayout *topLayout_;
TimelineViewManager *view_manager_; TimelineViewManager *view_manager_;
QTimer connectivityTimer_; QTimer connectivityTimer_;
std::atomic_bool isConnected_; std::atomic_bool isConnected_;
// Global user settings. // Global user settings.
QSharedPointer<UserSettings> userSettings_; QSharedPointer<UserSettings> userSettings_;
NotificationsManager notificationsManager; NotificationsManager notificationsManager;
CallManager *callManager_; CallManager *callManager_;
}; };
template<class Collection> template<class Collection>
std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>>
ChatPage::getMemberships(const std::vector<Collection> &collection) const ChatPage::getMemberships(const std::vector<Collection> &collection) const
{ {
std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> memberships; std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> memberships;
using Member = mtx::events::StateEvent<mtx::events::state::Member>; using Member = mtx::events::StateEvent<mtx::events::state::Member>;
for (const auto &event : collection) { for (const auto &event : collection) {
if (auto member = std::get_if<Member>(event)) { if (auto member = std::get_if<Member>(event)) {
memberships.emplace(member->state_key, *member); memberships.emplace(member->state_key, *member);
}
} }
}
return memberships; return memberships;
} }

View File

@ -10,18 +10,17 @@
Clipboard::Clipboard(QObject *parent) Clipboard::Clipboard(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
connect( connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged);
QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged);
} }
void void
Clipboard::setText(QString text) Clipboard::setText(QString text)
{ {
QGuiApplication::clipboard()->setText(text); QGuiApplication::clipboard()->setText(text);
} }
QString QString
Clipboard::text() const Clipboard::text() const
{ {
return QGuiApplication::clipboard()->text(); return QGuiApplication::clipboard()->text();
} }

View File

@ -9,14 +9,14 @@
class Clipboard : public QObject class Clipboard : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
public: public:
Clipboard(QObject *parent = nullptr); Clipboard(QObject *parent = nullptr);
QString text() const; QString text() const;
void setText(QString text_); void setText(QString text_);
signals: signals:
void textChanged(); void textChanged();
}; };

View File

@ -9,23 +9,23 @@
QPixmap QPixmap
ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &) ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &)
{ {
auto args = id.split('?'); auto args = id.split('?');
QPixmap source(args[0]); QPixmap source(args[0]);
if (size) if (size)
*size = QSize(source.width(), source.height()); *size = QSize(source.width(), source.height());
if (args.size() < 2) if (args.size() < 2)
return source; return source;
QColor color(args[1]); QColor color(args[1]);
QPixmap colorized = source; QPixmap colorized = source;
QPainter painter(&colorized); QPainter painter(&colorized);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn); painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(colorized.rect(), color); painter.fillRect(colorized.rect(), color);
painter.end(); painter.end();
return colorized; return colorized;
} }

View File

@ -7,9 +7,9 @@
class ColorImageProvider : public QQuickImageProvider class ColorImageProvider : public QQuickImageProvider
{ {
public: public:
ColorImageProvider() ColorImageProvider()
: QQuickImageProvider(QQuickImageProvider::Pixmap) : QQuickImageProvider(QQuickImageProvider::Pixmap)
{} {}
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
}; };

Some files were not shown because too many files have changed in this diff Show More