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,30 +326,37 @@ 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
@ -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 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 {
} }
] ]
Item {
anchors.fill: parent
TapHandler { TapHandler {
margin: -Nheko.paddingSmall
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onSingleTapped: communityContextMenu.show(model.id) onSingleTapped: communityContextMenu.show(model.id)
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
} }
TapHandler {
margin: -Nheko.paddingSmall
onSingleTapped: Communities.setCurrentTagId(model.id)
onLongPressed: communityContextMenu.show(model.id)
} }
HoverHandler { onClicked: Communities.setCurrentTagId(model.id)
id: hovered onPressAndHold: communityContextMenu.show(model.id)
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,14 +80,14 @@ 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;
completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true; event.accepted = true;
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
completerPopup.up();
else
completerPopup.down(); completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion(); completerPopup.finishCompletion();

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,6 +195,9 @@ 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) {
if (event.modifiers & Qt.ShiftModifier)
popup.down();
else
popup.up(); popup.up();
} else { } else {
var pos = cursorPosition - 1; var pos = cursorPosition - 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,14 +39,14 @@ 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;
completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true; event.accepted = true;
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
completerPopup.up();
else
completerPopup.down(); completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion(); completerPopup.finishCompletion();

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
Layout.fillWidth: true
sourceComponent: MessageView {
implicitHeight: msgView.height - typingIndicator.height implicitHeight: msgView.height - typingIndicator.height
} Layout.fillWidth: true
} }
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,6 +55,18 @@ Item {
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
} }
}
MxcAnimatedImage {
id: mxcimage
visible: loaded
anchors.fill: parent
roomm: room
play: !Settings.animateImagesOnHover || mouseArea.hovered
eventId: parent.eventId
}
HoverHandler { HoverHandler {
id: mouseArea id: mouseArea
} }
@ -87,6 +101,4 @@ Item {
} }
}
} }

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,6 +361,9 @@ Item {
DelegateChoice { DelegateChoice {
roleValue: MtxEvent.Member roleValue: MtxEvent.Member
ColumnLayout {
width: parent ? parent.width : undefined
NoticeMessage { NoticeMessage {
body: formatted body: formatted
isOnlyEmoji: false isOnlyEmoji: false
@ -364,6 +371,15 @@ Item {
formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId) 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)
}
}
} }
DelegateChoice { DelegateChoice {

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,25 +40,15 @@ 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) {
progress.updatePositionTexts();
} }
Connections {
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)
} }
} }
@ -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,6 +23,9 @@ Pane {
text: { text: {
if (flow.sender) { if (flow.sender) {
if (flow.isSelfVerification) if (flow.isSelfVerification)
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); 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.");

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

@ -48,8 +48,7 @@ resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
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);
}); });

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

@ -47,6 +47,11 @@ struct GroupSessionData
uint64_t message_index = 0; uint64_t message_index = 0;
uint64_t timestamp = 0; uint64_t timestamp = 0;
// If we got the session via key sharing or forwarding, we can usually trust it.
// 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;
std::string sender_claimed_ed25519_key; std::string sender_claimed_ed25519_key;
std::vector<std::string> forwarding_curve25519_key_chain; std::vector<std::string> forwarding_curve25519_key_chain;
@ -83,6 +88,13 @@ from_json(const nlohmann::json &obj, DevicePublicKeys &msg);
//! Represents a unique megolm session identifier. //! Represents a unique megolm session identifier.
struct MegolmSessionIndex struct MegolmSessionIndex
{ {
MegolmSessionIndex() = default;
MegolmSessionIndex(std::string room_id_, const mtx::events::msg::Encrypted &e)
: room_id(std::move(room_id_))
, session_id(e.session_id)
, sender_key(e.sender_key)
{}
//! The room in which this session exists. //! The room in which this session exists.
std::string room_id; std::string room_id;
//! The session_id of the megolm session. //! The session_id of the megolm session.
@ -167,3 +179,16 @@ 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

@ -93,7 +93,7 @@ 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;

View file

@ -49,15 +49,12 @@ public:
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys( std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
const std::string &room_id, const std::string &room_id,
bool verified_only); bool verified_only);
void updateUserKeys(const std::string &sync_token, void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
const mtx::responses::QueryKeys &keyQuery); void markUserKeysOutOfDate(const std::vector<std::string> &user_ids);
void markUserKeysOutOfDate(lmdb::txn &txn, void markUserKeysOutOfDate(lmdb::txn &txn,
lmdb::dbi &db, lmdb::dbi &db,
const std::vector<std::string> &user_ids, const std::vector<std::string> &user_ids,
const std::string &sync_token); const std::string &sync_token);
void deleteUserKeys(lmdb::txn &txn,
lmdb::dbi &db,
const std::vector<std::string> &user_ids);
void query_keys(const std::string &user_id, void query_keys(const std::string &user_id,
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb); std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
@ -109,6 +106,10 @@ public:
std::vector<RoomMember> getMembers(const std::string &room_id, std::vector<RoomMember> getMembers(const std::string &room_id,
std::size_t startIndex = 0, std::size_t startIndex = 0,
std::size_t len = 30); std::size_t len = 30);
std::vector<RoomMember> getMembersFromInvite(const std::string &room_id,
std::size_t startIndex = 0,
std::size_t len = 30);
size_t memberCount(const std::string &room_id); size_t memberCount(const std::string &room_id);
void saveState(const mtx::responses::Sync &res); void saveState(const mtx::responses::Sync &res);
@ -146,9 +147,7 @@ public:
//! There should be only one user id present in a receipt list per room. //! There should be only one user id present in a receipt list per room.
//! The user id should be removed from any other lists. //! The user id should be removed from any other lists.
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
void updateReadReceipt(lmdb::txn &txn, void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts);
const std::string &room_id,
const Receipts &receipts);
//! Retrieve all the read receipts for the given event id and room. //! Retrieve all the read receipts for the given event id and room.
//! //!
@ -186,8 +185,7 @@ public:
uint64_t index = std::numeric_limits<uint64_t>::max(), uint64_t index = std::numeric_limits<uint64_t>::max(),
bool forward = false); bool forward = false);
std::optional<mtx::events::collections::TimelineEvent> getEvent( std::optional<mtx::events::collections::TimelineEvent> getEvent(const std::string &room_id,
const std::string &room_id,
const std::string &event_id); const std::string &event_id);
void storeEvent(const std::string &room_id, void storeEvent(const std::string &room_id,
const std::string &event_id, const std::string &event_id,
@ -195,24 +193,20 @@ public:
void replaceEvent(const std::string &room_id, void replaceEvent(const std::string &room_id,
const std::string &event_id, const std::string &event_id,
const mtx::events::collections::TimelineEvent &event); const mtx::events::collections::TimelineEvent &event);
std::vector<std::string> relatedEvents(const std::string &room_id, std::vector<std::string> relatedEvents(const std::string &room_id, const std::string &event_id);
const std::string &event_id);
struct TimelineRange struct TimelineRange
{ {
uint64_t first, last; uint64_t first, last;
}; };
std::optional<TimelineRange> getTimelineRange(const std::string &room_id); std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
std::optional<uint64_t> getTimelineIndex(const std::string &room_id, std::optional<uint64_t> getTimelineIndex(const std::string &room_id, std::string_view event_id);
std::string_view event_id); std::optional<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id);
std::optional<uint64_t> getEventIndex(const std::string &room_id,
std::string_view event_id);
std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter( std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter(
const std::string &room_id, const std::string &room_id,
std::string_view event_id); std::string_view event_id);
std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index); std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
std::optional<uint64_t> getArrivalIndex(const std::string &room_id, std::optional<uint64_t> getArrivalIndex(const std::string &room_id, std::string_view event_id);
std::string_view event_id);
std::string previousBatchToken(const std::string &room_id); std::string previousBatchToken(const std::string &room_id);
uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res); uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
@ -267,8 +261,7 @@ public:
void saveInboundMegolmSession(const MegolmSessionIndex &index, void saveInboundMegolmSession(const MegolmSessionIndex &index,
mtx::crypto::InboundGroupSessionPtr session, mtx::crypto::InboundGroupSessionPtr session,
const GroupSessionData &data); const GroupSessionData &data);
mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession( mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index);
const MegolmSessionIndex &index);
bool inboundMegolmSessionExists(const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index);
std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index); std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index);
@ -281,15 +274,20 @@ public:
std::vector<std::string> getOlmSessions(const std::string &curve25519); std::vector<std::string> getOlmSessions(const std::string &curve25519);
std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519,
const std::string &session_id); const std::string &session_id);
std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession( std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(const std::string &curve25519);
const std::string &curve25519);
void saveOlmAccount(const std::string &pickled); void saveOlmAccount(const std::string &pickled);
std::string restoreOlmAccount(); std::string restoreOlmAccount();
void storeSecret(const std::string name, const std::string secret); void saveBackupVersion(const OnlineBackupVersion &data);
void deleteSecret(const std::string name); void deleteBackupVersion();
std::optional<std::string> secret(const std::string name); std::optional<OnlineBackupVersion> backupVersion();
void storeSecret(const std::string name, const std::string secret, bool internal = false);
void deleteSecret(const std::string name, bool internal = false);
std::optional<std::string> secret(const std::string name, bool internal = false);
std::string pickleSecret();
template<class T> template<class T>
constexpr static bool isStateEvent_ = constexpr static bool isStateEvent_ =
@ -300,21 +298,19 @@ public:
{ {
auto get_skey = [](const MDB_val *v) { auto get_skey = [](const MDB_val *v) {
return nlohmann::json::parse( return nlohmann::json::parse(
std::string_view(static_cast<const char *>(v->mv_data), std::string_view(static_cast<const char *>(v->mv_data), v->mv_size))
v->mv_size))
.value("key", ""); .value("key", "");
}; };
return get_skey(a).compare(get_skey(b)); return get_skey(a).compare(get_skey(b));
} }
signals: signals:
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void roomReadStatus(const std::map<QString, bool> &status); void roomReadStatus(const std::map<QString, bool> &status);
void removeNotification(const QString &room_id, const QString &event_id); void removeNotification(const QString &room_id, const QString &event_id);
void userKeysUpdate(const std::string &sync_token, void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
const mtx::responses::QueryKeys &keyQuery);
void verificationStatusChanged(const std::string &userid); void verificationStatusChanged(const std::string &userid);
void selfVerificationStatusChanged();
void secretChanged(const std::string name); void secretChanged(const std::string name);
private: private:
@ -388,9 +384,8 @@ private:
// //
case Membership::Invite: case Membership::Invite:
case Membership::Join: { case Membership::Join: {
auto display_name = e->content.display_name.empty() auto display_name =
? e->state_key e->content.display_name.empty() ? e->state_key : e->content.display_name;
: e->content.display_name;
// Lightweight representation of a member. // Lightweight representation of a member.
MemberInfo tmp{display_name, e->content.avatar_url}; MemberInfo tmp{display_name, e->content.avatar_url};
@ -416,17 +411,14 @@ private:
eventsDb.put(txn, e.event_id, json(e).dump()); eventsDb.put(txn, e.event_id, json(e).dump());
if (e.type != EventType::Unsupported) { if (e.type != EventType::Unsupported) {
if (std::is_same_v< if (std::is_same_v<std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
std::remove_cv_t<
std::remove_reference_t<decltype(e)>>,
StateEvent<mtx::events::msg::Redacted>>) { StateEvent<mtx::events::msg::Redacted>>) {
if (e.type == EventType::RoomMember) if (e.type == EventType::RoomMember)
membersdb.del(txn, e.state_key, ""); membersdb.del(txn, e.state_key, "");
else if (e.state_key.empty()) else if (e.state_key.empty())
statesdb.del(txn, to_string(e.type)); statesdb.del(txn, to_string(e.type));
else else
stateskeydb.del( stateskeydb.del(txn,
txn,
to_string(e.type), to_string(e.type),
json::object({ json::object({
{"key", e.state_key}, {"key", e.state_key},
@ -434,11 +426,9 @@ private:
}) })
.dump()); .dump());
} else if (e.state_key.empty()) } else if (e.state_key.empty())
statesdb.put( statesdb.put(txn, to_string(e.type), json(e).dump());
txn, to_string(e.type), json(e).dump());
else else
stateskeydb.put( stateskeydb.put(txn,
txn,
to_string(e.type), to_string(e.type),
json::object({ json::object({
{"key", e.state_key}, {"key", e.state_key},
@ -482,8 +472,7 @@ private:
try { try {
auto eventsDb = getEventsDb(txn, room_id); auto eventsDb = getEventsDb(txn, room_id);
if (!eventsDb.get( if (!eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value))
txn, json::parse(data)["id"].get<std::string>(), value))
return std::nullopt; return std::nullopt;
} catch (std::exception &e) { } catch (std::exception &e) {
return std::nullopt; return std::nullopt;
@ -522,16 +511,11 @@ private:
auto cursor = lmdb::cursor::open(txn, db); auto cursor = lmdb::cursor::open(txn, db);
bool first = true; bool first = true;
if (cursor.get(typeStrV, data, MDB_SET)) { if (cursor.get(typeStrV, data, MDB_SET)) {
while (cursor.get( while (cursor.get(typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
first = false; first = false;
if (eventsDb.get(txn, if (eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value))
json::parse(data)["id"].get<std::string>(), events.push_back(json::parse(value).get<mtx::events::StateEvent<T>>());
value))
events.push_back(
json::parse(value)
.get<mtx::events::StateEvent<T>>());
} }
} }
} }
@ -580,14 +564,12 @@ private:
// inverse of EventOrderDb // inverse of EventOrderDb
lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
{ {
return lmdb::dbi::open( return lmdb::dbi::open(txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
} }
lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
{ {
return lmdb::dbi::open( return lmdb::dbi::open(txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
} }
lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
@ -610,14 +592,12 @@ private:
lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
{ {
return lmdb::dbi::open( return lmdb::dbi::open(txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
} }
lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
{ {
return lmdb::dbi::open( return lmdb::dbi::open(txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
} }
lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
@ -635,8 +615,7 @@ private:
lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
{ {
return lmdb::dbi::open( return lmdb::dbi::open(txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
} }
lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
@ -649,15 +628,9 @@ private:
return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE);
} }
lmdb::dbi getPresenceDb(lmdb::txn &txn) lmdb::dbi getPresenceDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "presence", MDB_CREATE); }
{
return lmdb::dbi::open(txn, "presence", MDB_CREATE);
}
lmdb::dbi getUserKeysDb(lmdb::txn &txn) lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); }
{
return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
}
lmdb::dbi getVerificationDb(lmdb::txn &txn) lmdb::dbi getVerificationDb(lmdb::txn &txn)
{ {
@ -682,13 +655,11 @@ private:
return QString::fromStdString(event.state_key); return QString::fromStdString(event.state_key);
} }
std::optional<VerificationCache> verificationCache(const std::string &user_id, std::optional<VerificationCache> verificationCache(const std::string &user_id, lmdb::txn &txn);
lmdb::txn &txn);
VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn); VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn); std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn);
void setNextBatchToken(lmdb::txn &txn, const std::string &token); void setNextBatchToken(lmdb::txn &txn, const std::string &token);
void setNextBatchToken(lmdb::txn &txn, const QString &token);
lmdb::env env_; lmdb::env env_;
lmdb::dbi syncStateDb_; lmdb::dbi syncStateDb_;
@ -710,6 +681,8 @@ private:
QString localUserId_; QString localUserId_;
QString cacheDirectory_; QString cacheDirectory_;
std::string pickle_secret_;
VerificationStorage verification_storage; VerificationStorage verification_storage;
bool databaseReady_ = false; bool databaseReady_ = false;

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

View file

@ -6,26 +6,25 @@
#include <QApplication> #include <QApplication>
#include <QInputDialog> #include <QInputDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QSettings>
#include <mtx/responses.hpp> #include <mtx/responses.hpp>
#include "AvatarProvider.h" #include "AvatarProvider.h"
#include "Cache.h" #include "Cache.h"
#include "Cache_p.h" #include "Cache_p.h"
#include "CallManager.h"
#include "ChatPage.h" #include "ChatPage.h"
#include "DeviceVerificationFlow.h"
#include "EventAccessors.h" #include "EventAccessors.h"
#include "Logging.h" #include "Logging.h"
#include "MainWindow.h" #include "MainWindow.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "Olm.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "Utils.h" #include "Utils.h"
#include "encryption/DeviceVerificationFlow.h"
#include "encryption/Olm.h"
#include "ui/OverlayModal.h" #include "ui/OverlayModal.h"
#include "ui/Theme.h" #include "ui/Theme.h"
#include "ui/UserProfile.h" #include "ui/UserProfile.h"
#include "voip/CallManager.h"
#include "notifications/Manager.h" #include "notifications/Manager.h"
@ -33,9 +32,6 @@
#include "blurhash.hpp" #include "blurhash.hpp"
// TODO: Needs to be updated with an actual secret.
static const std::string STORAGE_SECRET_KEY("secret");
ChatPage *ChatPage::instance_ = nullptr; ChatPage *ChatPage::instance_ = nullptr;
constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
constexpr int RETRY_TIMEOUT = 5'000; constexpr int RETRY_TIMEOUT = 5'000;
@ -125,11 +121,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
http::client()->invite_user( http::client()->invite_user(
roomId.toStdString(), roomId.toStdString(),
user.toStdString(), user.toStdString(),
[this, user](const mtx::responses::RoomInvite &, [this, user](const mtx::responses::RoomInvite &, mtx::http::RequestErr err) {
mtx::http::RequestErr err) {
if (err) { if (err) {
emit showNotification( emit showNotification(tr("Failed to invite user: %1").arg(user));
tr("Failed to invite user: %1").arg(user));
return; return;
} }
@ -140,7 +134,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
}); });
connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
connect(this, connect(this,
&ChatPage::highlightedNotifsRetrieved, &ChatPage::highlightedNotifsRetrieved,
@ -193,22 +187,33 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
view_manager_->sync(rooms); view_manager_->sync(rooms);
bool hasNotifications = false; static unsigned int prevNotificationCount = 0;
unsigned int notificationCount = 0;
for (const auto &room : rooms.join) { for (const auto &room : rooms.join) {
if (room.second.unread_notifications.notification_count > 0) notificationCount += room.second.unread_notifications.notification_count;
hasNotifications = true;
} }
if (hasNotifications && userSettings_->hasNotifications()) // HACK: If we had less notifications last time we checked, send an alert if the
// user wanted one. Technically, this may cause an alert to be missed if new ones
// come in while you are reading old ones. Since the window is almost certainly open
// in this edge case, that's probably a non-issue.
// TODO: Replace this once we have proper pushrules support. This is a horrible hack
if (prevNotificationCount < notificationCount) {
if (userSettings_->hasAlertOnNotification())
QApplication::alert(this);
}
prevNotificationCount = notificationCount;
// No need to check amounts for this section, as this function internally checks for
// duplicates.
if (notificationCount && userSettings_->hasNotifications())
http::client()->notifications( http::client()->notifications(
5, 5,
"", "",
"", "",
[this](const mtx::responses::Notifications &res, [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) {
mtx::http::RequestErr err) {
if (err) { if (err) {
nhlog::net()->warn( nhlog::net()->warn("failed to retrieve notifications: {} ({})",
"failed to retrieve notifications: {} ({})",
err->matrix_error.error, err->matrix_error.error,
static_cast<int>(err->status_code)); static_cast<int>(err->status_code));
return; return;
@ -228,11 +233,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
[this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); }, [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
Qt::QueuedConnection); Qt::QueuedConnection);
connect(this, connect(
&ChatPage::newSyncResponse, this, &ChatPage::newSyncResponse, this, &ChatPage::handleSyncResponse, Qt::QueuedConnection);
this,
&ChatPage::handleSyncResponse,
Qt::QueuedConnection);
connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
@ -277,15 +279,15 @@ ChatPage::resetUI()
void void
ChatPage::deleteConfigs() ChatPage::deleteConfigs()
{ {
QSettings settings; auto settings = UserSettings::instance()->qsettings();
if (UserSettings::instance()->profile() != "") { if (UserSettings::instance()->profile() != "") {
settings.beginGroup("profile"); settings->beginGroup("profile");
settings.beginGroup(UserSettings::instance()->profile()); settings->beginGroup(UserSettings::instance()->profile());
} }
settings.beginGroup("auth"); settings->beginGroup("auth");
settings.remove(""); settings->remove("");
settings.endGroup(); // auth settings->endGroup(); // auth
http::client()->shutdown(); http::client()->shutdown();
cache::deleteData(); cache::deleteData();
@ -299,14 +301,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
try { try {
http::client()->set_user(parse<User>(userid.toStdString())); http::client()->set_user(parse<User>(userid.toStdString()));
} catch (const std::invalid_argument &) { } catch (const std::invalid_argument &) {
nhlog::ui()->critical("bootstrapped with invalid user_id: {}", nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString());
userid.toStdString());
} }
http::client()->set_server(homeserver.toStdString()); http::client()->set_server(homeserver.toStdString());
http::client()->set_access_token(token.toStdString()); http::client()->set_access_token(token.toStdString());
http::client()->verify_certificates( http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
!UserSettings::instance()->disableCertificateValidation());
// The Olm client needs the user_id & device_id that will be included // The Olm client needs the user_id & device_id that will be included
// in the generated payloads & keys. // in the generated payloads & keys.
@ -339,8 +339,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
return; return;
} else if (cacheVersion == cache::CacheVersion::Older) { } else if (cacheVersion == cache::CacheVersion::Older) {
if (!cache::runMigrations()) { if (!cache::runMigrations()) {
QMessageBox::critical( QMessageBox::critical(this,
this,
tr("Cache migration failed!"), tr("Cache migration failed!"),
tr("Migrating the cache to the current version failed. " tr("Migrating the cache to the current version failed. "
"This can have different reasons. Please open an " "This can have different reasons. Please open an "
@ -373,7 +372,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
// There isn't a saved olm account to restore. // There isn't a saved olm account to restore.
nhlog::crypto()->info("creating new olm account"); nhlog::crypto()->info("creating new olm account");
olm::client()->create_new_account(); olm::client()->create_new_account();
cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret()));
} catch (const lmdb::error &e) { } catch (const lmdb::error &e) {
nhlog::crypto()->critical("failed to save olm account {}", e.what()); nhlog::crypto()->critical("failed to save olm account {}", e.what());
emit dropToLoginPageCb(QString::fromStdString(e.what())); emit dropToLoginPageCb(QString::fromStdString(e.what()));
@ -385,6 +384,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
} }
getProfileInfo(); getProfileInfo();
getBackupVersion();
tryInitialSync(); tryInitialSync();
} }
@ -394,7 +394,7 @@ ChatPage::loadStateFromCache()
nhlog::db()->info("restoring state from cache"); nhlog::db()->info("restoring state from cache");
try { try {
olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret());
emit initializeEmptyViews(); emit initializeEmptyViews();
emit initializeMentions(cache::getTimelineMentions()); emit initializeMentions(cache::getTimelineMentions());
@ -411,6 +411,11 @@ ChatPage::loadStateFromCache()
return; return;
} catch (const json::exception &e) { } catch (const json::exception &e) {
nhlog::db()->critical("failed to parse cache data: {}", e.what()); nhlog::db()->critical("failed to parse cache data: {}", e.what());
emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
return;
} catch (const std::exception &e) {
nhlog::db()->critical("failed to load cache data: {}", e.what());
emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
return; return;
} }
@ -418,6 +423,8 @@ ChatPage::loadStateFromCache()
nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
getProfileInfo(); getProfileInfo();
getBackupVersion();
verifyOneTimeKeyCountAfterStartup();
emit contentLoaded(); emit contentLoaded();
@ -459,15 +466,10 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
if (isRoomActive(room_id)) if (isRoomActive(room_id))
continue; continue;
if (userSettings_->hasAlertOnNotification()) {
QApplication::alert(this);
}
if (userSettings_->hasDesktopNotifications()) { if (userSettings_->hasDesktopNotifications()) {
auto info = cache::singleRoomInfo(item.room_id); auto info = cache::singleRoomInfo(item.room_id);
AvatarProvider::resolve( AvatarProvider::resolve(QString::fromStdString(info.avatar_url),
QString::fromStdString(info.avatar_url),
96, 96,
this, this,
[this, item](QPixmap image) { [this, item](QPixmap image) {
@ -499,14 +501,12 @@ ChatPage::tryInitialSync()
const int status_code = static_cast<int>(err->status_code); const int status_code = static_cast<int>(err->status_code);
if (status_code == 404) { if (status_code == 404) {
nhlog::net()->warn( nhlog::net()->warn("skipping key uploading. server doesn't provide /keys/upload");
"skipping key uploading. server doesn't provide /keys/upload");
return startInitialSync(); return startInitialSync();
} }
nhlog::crypto()->critical("failed to upload one time keys: {} {}", nhlog::crypto()->critical(
err->matrix_error.error, "failed to upload one time keys: {} {}", err->matrix_error.error, status_code);
status_code);
QString errorMsg(tr("Failed to setup encryption keys. Server response: " QString errorMsg(tr("Failed to setup encryption keys. Server response: "
"%1 %2. Please try again later.") "%1 %2. Please try again later.")
@ -520,8 +520,7 @@ ChatPage::tryInitialSync()
olm::mark_keys_as_published(); olm::mark_keys_as_published();
for (const auto &entry : res.one_time_key_counts) for (const auto &entry : res.one_time_key_counts)
nhlog::net()->info( nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first);
"uploaded {} {} one-time keys", entry.second, entry.first);
startInitialSync(); startInitialSync();
}); });
@ -536,8 +535,7 @@ ChatPage::startInitialSync()
opts.timeout = 0; opts.timeout = 0;
opts.set_presence = currentPresence(); opts.set_presence = currentPresence();
http::client()->sync( http::client()->sync(opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
// TODO: Initial Sync should include mentions as well... // TODO: Initial Sync should include mentions as well...
if (err) { if (err) {
@ -584,8 +582,7 @@ ChatPage::startInitialSync()
cache::calculateRoomReadStatus(); cache::calculateRoomReadStatus();
} catch (const lmdb::error &e) { } catch (const lmdb::error &e) {
nhlog::db()->error("failed to save state after initial sync: {}", nhlog::db()->error("failed to save state after initial sync: {}", e.what());
e.what());
startInitialSync(); startInitialSync();
return; return;
} }
@ -655,8 +652,7 @@ ChatPage::trySync()
} }
http::client()->sync( http::client()->sync(
opts, opts, [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
[this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
if (err) { if (err) {
const auto error = QString::fromStdString(err->matrix_error.error); const auto error = QString::fromStdString(err->matrix_error.error);
const auto msg = tr("Please try to login again: %1").arg(error); const auto msg = tr("Please try to login again: %1").arg(error);
@ -664,10 +660,8 @@ ChatPage::trySync()
const int status_code = static_cast<int>(err->status_code); const int status_code = static_cast<int>(err->status_code);
if ((http::is_logged_in() && if ((http::is_logged_in() &&
(err->matrix_error.errcode == (err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
mtx::errors::ErrorCode::M_UNKNOWN_TOKEN || err->matrix_error.errcode == mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
err->matrix_error.errcode ==
mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
!http::is_logged_in()) { !http::is_logged_in()) {
emit dropToLoginPageCb(msg); emit dropToLoginPageCb(msg);
return; return;
@ -710,8 +704,7 @@ ChatPage::joinRoomVia(const std::string &room_id,
room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) { room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
if (err) { if (err) {
emit showNotification( emit showNotification(
tr("Failed to join room: %1") tr("Failed to join room: %1").arg(QString::fromStdString(err->matrix_error.error)));
.arg(QString::fromStdString(err->matrix_error.error)));
return; return;
} }
@ -738,8 +731,7 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
const auto error = err->matrix_error.error; const auto error = err->matrix_error.error;
const int status_code = static_cast<int>(err->status_code); const int status_code = static_cast<int>(err->status_code);
nhlog::net()->warn( nhlog::net()->warn("failed to create room: {} {} ({})", error, err_code, status_code);
"failed to create room: {} {} ({})", error, err_code, status_code);
emit showNotification( emit showNotification(
tr("Room creation failed: %1").arg(QString::fromStdString(error))); tr("Room creation failed: %1").arg(QString::fromStdString(error)));
@ -749,6 +741,7 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
QString newRoomId = QString::fromStdString(res.room_id.to_string()); QString newRoomId = QString::fromStdString(res.room_id.to_string());
emit showNotification(tr("Room %1 created.").arg(newRoomId)); emit showNotification(tr("Room %1 created.").arg(newRoomId));
emit newRoom(newRoomId); emit newRoom(newRoomId);
emit changeToRoom(newRoomId);
}); });
} }
@ -759,8 +752,7 @@ ChatPage::leaveRoom(const QString &room_id)
room_id.toStdString(), room_id.toStdString(),
[this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) { [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) { if (err) {
emit showNotification( emit showNotification(tr("Failed to leave room: %1")
tr("Failed to leave room: %1")
.arg(QString::fromStdString(err->matrix_error.error))); .arg(QString::fromStdString(err->matrix_error.error)));
return; return;
} }
@ -792,8 +784,7 @@ ChatPage::inviteUser(QString userid, QString reason)
userid.toStdString(), userid.toStdString(),
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) { if (err) {
emit showNotification( emit showNotification(tr("Failed to invite %1 to %2: %3")
tr("Failed to invite %1 to %2: %3")
.arg(userid) .arg(userid)
.arg(room) .arg(room)
.arg(QString::fromStdString(err->matrix_error.error))); .arg(QString::fromStdString(err->matrix_error.error)));
@ -819,8 +810,7 @@ ChatPage::kickUser(QString userid, QString reason)
userid.toStdString(), userid.toStdString(),
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) { if (err) {
emit showNotification( emit showNotification(tr("Failed to kick %1 from %2: %3")
tr("Failed to kick %1 from %2: %3")
.arg(userid) .arg(userid)
.arg(room) .arg(room)
.arg(QString::fromStdString(err->matrix_error.error))); .arg(QString::fromStdString(err->matrix_error.error)));
@ -846,8 +836,7 @@ ChatPage::banUser(QString userid, QString reason)
userid.toStdString(), userid.toStdString(),
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) { if (err) {
emit showNotification( emit showNotification(tr("Failed to ban %1 in %2: %3")
tr("Failed to ban %1 in %2: %3")
.arg(userid) .arg(userid)
.arg(room) .arg(room)
.arg(QString::fromStdString(err->matrix_error.error))); .arg(QString::fromStdString(err->matrix_error.error)));
@ -873,8 +862,7 @@ ChatPage::unbanUser(QString userid, QString reason)
userid.toStdString(), userid.toStdString(),
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) { if (err) {
emit showNotification( emit showNotification(tr("Failed to unban %1 in %2: %3")
tr("Failed to unban %1 in %2: %3")
.arg(userid) .arg(userid)
.arg(room) .arg(room)
.arg(QString::fromStdString(err->matrix_error.error))); .arg(QString::fromStdString(err->matrix_error.error)));
@ -902,8 +890,7 @@ ChatPage::setStatus(const QString &status)
http::client()->put_presence_status( http::client()->put_presence_status(
currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) { currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) {
if (err) { if (err) {
nhlog::net()->warn("failed to set presence status_msg: {}", nhlog::net()->warn("failed to set presence status_msg: {}", err->matrix_error.error);
err->matrix_error.error);
} }
}); });
} }
@ -924,17 +911,49 @@ ChatPage::currentPresence() const
} }
void void
ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts) ChatPage::verifyOneTimeKeyCountAfterStartup()
{ {
uint16_t count = 0; http::client()->upload_keys(
if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end()) olm::client()->create_upload_keys_request(),
count = c->second; [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
if (err) {
nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
err->matrix_error.error,
static_cast<int>(err->status_code),
static_cast<int>(err->error_code));
if (count < MAX_ONETIME_KEYS) { if (err->status_code < 400 || err->status_code >= 500)
const int nkeys = MAX_ONETIME_KEYS - count; return;
}
std::map<std::string, uint16_t> key_counts;
auto count = 0;
if (auto c = res.one_time_key_counts.find(mtx::crypto::SIGNED_CURVE25519);
c == res.one_time_key_counts.end()) {
key_counts[mtx::crypto::SIGNED_CURVE25519] = 0;
} else {
key_counts[mtx::crypto::SIGNED_CURVE25519] = c->second;
count = c->second;
}
nhlog::crypto()->info( nhlog::crypto()->info(
"uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519); "Fetched server key count {} {}", count, mtx::crypto::SIGNED_CURVE25519);
ensureOneTimeKeyCount(key_counts);
});
}
void
ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
{
if (auto count = counts.find(mtx::crypto::SIGNED_CURVE25519); count != counts.end()) {
nhlog::crypto()->debug(
"Updated server key count {} {}", count->second, mtx::crypto::SIGNED_CURVE25519);
if (count->second < MAX_ONETIME_KEYS) {
const int nkeys = MAX_ONETIME_KEYS - count->second;
nhlog::crypto()->info("uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
olm::client()->generate_one_time_keys(nkeys); olm::client()->generate_one_time_keys(nkeys);
http::client()->upload_keys( http::client()->upload_keys(
@ -953,6 +972,22 @@ ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
// mark as published anyway, otherwise we may end up in a loop. // mark as published anyway, otherwise we may end up in a loop.
olm::mark_keys_as_published(); olm::mark_keys_as_published();
}); });
} else if (count->second > 2 * MAX_ONETIME_KEYS) {
nhlog::crypto()->warn("too many one-time keys, deleting 1");
mtx::requests::ClaimKeys req;
req.one_time_keys[http::client()->user_id().to_string()][http::client()->device_id()] =
std::string(mtx::crypto::SIGNED_CURVE25519);
http::client()->claim_keys(
req, [](const mtx::responses::ClaimKeys &, mtx::http::RequestErr err) {
if (err)
nhlog::crypto()->warn("failed to clear 1 one-time key: {} {} {}",
err->matrix_error.error,
static_cast<int>(err->status_code),
static_cast<int>(err->error_code));
else
nhlog::crypto()->info("cleared 1 one-time key");
});
}
} }
} }
@ -974,6 +1009,60 @@ ChatPage::getProfileInfo()
}); });
} }
void
ChatPage::getBackupVersion()
{
if (!UserSettings::instance()->useOnlineKeyBackup()) {
nhlog::crypto()->info("Online key backup disabled.");
return;
}
http::client()->backup_version(
[this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("Failed to retrieve backup version");
if (err->status_code == 404)
cache::client()->deleteBackupVersion();
return;
}
// switch to UI thread for secrets stuff
QTimer::singleShot(0, this, [res] {
auto auth_data = nlohmann::json::parse(res.auth_data);
if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
auto key = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
if (!key) {
nhlog::crypto()->info("No key for online key backup.");
cache::client()->deleteBackupVersion();
return;
}
using namespace mtx::crypto;
auto pubkey = CURVE25519_public_key_from_private(to_binary_buf(base642bin(*key)));
if (auth_data["public_key"].get<std::string>() != pubkey) {
nhlog::crypto()->info("Our backup key {} does not match the one "
"used in the online backup {}",
pubkey,
auth_data["public_key"]);
cache::client()->deleteBackupVersion();
return;
}
nhlog::crypto()->info("Using online key backup.");
OnlineBackupVersion data{};
data.algorithm = res.algorithm;
data.version = res.version;
cache::client()->saveBackupVersion(data);
} else {
nhlog::crypto()->info("Unsupported key backup algorithm: {}", res.algorithm);
cache::client()->deleteBackupVersion();
}
});
});
}
void void
ChatPage::initiateLogout() ChatPage::initiateLogout()
{ {
@ -1012,8 +1101,7 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"), QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
keyDesc.name.empty() keyDesc.name.empty()
? QCoreApplication::translate( ? QCoreApplication::translate(
"CrossSigningSecrets", "CrossSigningSecrets", "Enter your recovery key or passphrase to decrypt your secrets:")
"Enter your recovery key or passphrase to decrypt your secrets:")
: QCoreApplication::translate( : QCoreApplication::translate(
"CrossSigningSecrets", "CrossSigningSecrets",
"Enter your recovery key or passphrase called %1 to decrypt your secrets:") "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
@ -1027,11 +1115,9 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
if (!decryptionKey && keyDesc.passphrase) { if (!decryptionKey && keyDesc.passphrase) {
try { try {
decryptionKey = decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
} catch (std::exception &e) { } catch (std::exception &e) {
nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", e.what());
e.what());
} }
} }
@ -1045,11 +1131,71 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
return; return;
} }
auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
mtx::requests::KeySignaturesUpload req;
for (const auto &[secretName, encryptedSecret] : secrets) { for (const auto &[secretName, encryptedSecret] : secrets) {
auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName); auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
if (!decrypted.empty()) if (!decrypted.empty()) {
cache::storeSecret(secretName, decrypted); cache::storeSecret(secretName, decrypted);
if (deviceKeys &&
secretName == mtx::secret_storage::secrets::cross_signing_self_signing) {
auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
if (myKey.user_id == http::client()->user_id().to_string() &&
myKey.device_id == http::client()->device_id() &&
myKey.keys["ed25519:" + http::client()->device_id()] ==
olm::client()->identity_keys().ed25519 &&
myKey.keys["curve25519:" + http::client()->device_id()] ==
olm::client()->identity_keys().curve25519) {
json j = myKey;
j.erase("signatures");
j.erase("unsigned");
auto ssk = mtx::crypto::PkSigning::from_seed(decrypted);
myKey.signatures[http::client()->user_id().to_string()]
["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
req.signatures[http::client()->user_id().to_string()]
[http::client()->device_id()] = myKey;
} }
} else if (deviceKeys &&
secretName == mtx::secret_storage::secrets::cross_signing_master) {
auto mk = mtx::crypto::PkSigning::from_seed(decrypted);
if (deviceKeys->master_keys.user_id == http::client()->user_id().to_string() &&
deviceKeys->master_keys.keys["ed25519:" + mk.public_key()] == mk.public_key()) {
json j = deviceKeys->master_keys;
j.erase("signatures");
j.erase("unsigned");
mtx::crypto::CrossSigningKeys master_key = j;
master_key.signatures[http::client()->user_id().to_string()]
["ed25519:" + http::client()->device_id()] =
olm::client()->sign_message(j.dump());
req.signatures[http::client()->user_id().to_string()][mk.public_key()] =
master_key;
}
}
}
}
if (!req.signatures.empty())
http::client()->keys_signatures_upload(
req, [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error("failed to upload signatures: {},{}",
mtx::errors::to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code));
}
for (const auto &[user_id, tmp] : res.errors)
for (const auto &[key_id, e] : tmp)
nhlog::net()->error("signature error for user '{}' and key "
"id {}: {}, {}",
user_id,
key_id,
mtx::errors::to_string(e.errcode),
e.error);
});
} }
void void
@ -1061,11 +1207,9 @@ ChatPage::startChat(QString userid)
for (std::string room_id : joined_rooms) { for (std::string room_id : joined_rooms) {
if (room_infos[QString::fromStdString(room_id)].member_count == 2) { if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
auto room_members = cache::roomMembers(room_id); auto room_members = cache::roomMembers(room_id);
if (std::find(room_members.begin(), if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) !=
room_members.end(), room_members.end()) {
(userid).toStdString()) != room_members.end()) { view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
view_manager_->rooms()->setCurrentRoom(
QString::fromStdString(room_id));
return; return;
} }
} }
@ -1109,14 +1253,54 @@ mxidFromSegments(QStringRef sigil, QStringRef mxid)
} }
} }
void bool
ChatPage::handleMatrixUri(const QByteArray &uri) ChatPage::handleMatrixUri(const QByteArray &uri)
{ {
nhlog::ui()->info("Received uri! {}", uri.toStdString()); nhlog::ui()->info("Received uri! {}", uri.toStdString());
QUrl uri_{QString::fromUtf8(uri)}; QUrl uri_{QString::fromUtf8(uri)};
// Convert matrix.to URIs to proper format
if (uri_.scheme() == "https" && uri_.host() == "matrix.to") {
QString p = uri_.fragment(QUrl::FullyEncoded);
if (p.startsWith("/"))
p.remove(0, 1);
auto temp = p.split("?");
QString query;
if (temp.size() >= 2)
query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
temp = temp.first().split("/");
auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
if (!identifier.isEmpty()) {
if (identifier.startsWith("@")) {
QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
if (!query.isEmpty())
newUri.append("?" + query.toUtf8());
return handleMatrixUri(QUrl::fromEncoded(newUri));
} else if (identifier.startsWith("#")) {
QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
if (!eventId.isEmpty())
newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
if (!query.isEmpty())
newUri.append("?" + query.toUtf8());
return handleMatrixUri(QUrl::fromEncoded(newUri));
} else if (identifier.startsWith("!")) {
QByteArray newUri =
"matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
if (!eventId.isEmpty())
newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
if (!query.isEmpty())
newUri.append("?" + query.toUtf8());
return handleMatrixUri(QUrl::fromEncoded(newUri));
}
}
}
// non-matrix URIs are not handled by us, return false
if (uri_.scheme() != "matrix") if (uri_.scheme() != "matrix")
return; return false;
auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded); auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
if (tempPath.startsWith('/')) if (tempPath.startsWith('/'))
@ -1124,17 +1308,17 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
auto segments = tempPath.splitRef('/'); auto segments = tempPath.splitRef('/');
if (segments.size() != 2 && segments.size() != 4) if (segments.size() != 2 && segments.size() != 4)
return; return false;
auto sigil1 = segments[0]; auto sigil1 = segments[0];
auto mxid1 = mxidFromSegments(sigil1, segments[1]); auto mxid1 = mxidFromSegments(sigil1, segments[1]);
if (mxid1.isEmpty()) if (mxid1.isEmpty())
return; return false;
QString mxid2; QString mxid2;
if (segments.size() == 4 && segments[2] == "e") { if (segments.size() == 4 && segments[2] == "e") {
if (segments[3].isEmpty()) if (segments[3].isEmpty())
return; return false;
else else
mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8()); mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
} }
@ -1148,18 +1332,22 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
if (item.startsWith("action=")) { if (item.startsWith("action=")) {
action = item.remove("action="); action = item.remove("action=");
} else if (item.startsWith("via=")) { } else if (item.startsWith("via=")) {
vias.push_back( vias.push_back(QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
} }
} }
if (sigil1 == "u") { if (sigil1 == "u") {
if (action.isEmpty()) { if (action.isEmpty()) {
if (auto t = view_manager_->rooms()->currentRoom()) auto t = view_manager_->rooms()->currentRoom();
if (t && cache::isRoomMember(mxid1.toStdString(), t->roomId().toStdString())) {
t->openUserProfile(mxid1); t->openUserProfile(mxid1);
return true;
}
emit view_manager_->openGlobalUserProfile(mxid1);
} else if (action == "chat") { } else if (action == "chat") {
this->startChat(mxid1); this->startChat(mxid1);
} }
return true;
} else if (sigil1 == "roomid") { } else if (sigil1 == "roomid") {
auto joined_rooms = cache::joinedRooms(); auto joined_rooms = cache::joinedRooms();
auto targetRoomId = mxid1.toStdString(); auto targetRoomId = mxid1.toStdString();
@ -1169,13 +1357,15 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
view_manager_->rooms()->setCurrentRoom(mxid1); view_manager_->rooms()->setCurrentRoom(mxid1);
if (!mxid2.isEmpty()) if (!mxid2.isEmpty())
view_manager_->showEvent(mxid1, mxid2); view_manager_->showEvent(mxid1, mxid2);
return; return true;
} }
} }
if (action == "join" || action.isEmpty()) { if (action == "join" || action.isEmpty()) {
joinRoomVia(targetRoomId, vias); joinRoomVia(targetRoomId, vias);
return true;
} }
return false;
} else if (sigil1 == "r") { } else if (sigil1 == "r") {
auto joined_rooms = cache::joinedRooms(); auto joined_rooms = cache::joinedRooms();
auto targetRoomAlias = mxid1.toStdString(); auto targetRoomAlias = mxid1.toStdString();
@ -1184,26 +1374,27 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
auto aliases = cache::client()->getRoomAliases(roomid); auto aliases = cache::client()->getRoomAliases(roomid);
if (aliases) { if (aliases) {
if (aliases->alias == targetRoomAlias) { if (aliases->alias == targetRoomAlias) {
view_manager_->rooms()->setCurrentRoom( view_manager_->rooms()->setCurrentRoom(QString::fromStdString(roomid));
QString::fromStdString(roomid));
if (!mxid2.isEmpty()) if (!mxid2.isEmpty())
view_manager_->showEvent( view_manager_->showEvent(QString::fromStdString(roomid), mxid2);
QString::fromStdString(roomid), mxid2); return true;
return;
} }
} }
} }
if (action == "join" || action.isEmpty()) { if (action == "join" || action.isEmpty()) {
joinRoomVia(mxid1.toStdString(), vias); joinRoomVia(mxid1.toStdString(), vias);
return true;
} }
return false;
} }
return false;
} }
void bool
ChatPage::handleMatrixUri(const QUrl &uri) ChatPage::handleMatrixUri(const QUrl &uri)
{ {
handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8()); return handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
} }
bool bool

View file

@ -78,8 +78,8 @@ public:
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);
@ -102,8 +102,7 @@ signals:
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();
@ -125,6 +124,7 @@ signals:
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();
@ -145,16 +145,13 @@ signals:
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(
const mtx::events::msg::KeyVerificationRequest &message,
std::string sender); std::string sender);
void receivedRoomDeviceVerificationRequest( void receivedRoomDeviceVerificationRequest(
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
TimelineModel *model); TimelineModel *model);
void receivedDeviceVerificationCancel( void receivedDeviceVerificationCancel(const mtx::events::msg::KeyVerificationCancel &message);
const mtx::events::msg::KeyVerificationCancel &message);
void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message); void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message); void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message, void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
@ -171,8 +168,7 @@ private slots:
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_;
@ -180,8 +176,10 @@ private:
void startInitialSync(); void startInitialSync();
void tryInitialSync(); void tryInitialSync();
void trySync(); void trySync();
void verifyOneTimeKeyCountAfterStartup();
void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts); void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
void getProfileInfo(); 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);

View file

@ -10,8 +10,7 @@
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

View file

@ -99,8 +99,8 @@ int
CompletionProxyModel::rowCount(const QModelIndex &) const CompletionProxyModel::rowCount(const QModelIndex &) const
{ {
if (searchString_.isEmpty()) if (searchString_.isEmpty())
return std::min(static_cast<int>(std::min<size_t>(max_completions_, return std::min(
std::numeric_limits<int>::max())), static_cast<int>(std::min<size_t>(max_completions_, std::numeric_limits<int>::max())),
sourceModel()->rowCount()); sourceModel()->rowCount());
else else
return (int)mapping.size(); return (int)mapping.size();

View file

@ -94,8 +94,7 @@ struct trie
if (keys.size() >= 2) { if (keys.size() >= 2) {
auto t = this; auto t = this;
for (int i = 1; i >= 0; i--) { for (int i = 1; i >= 0; i--) {
if (auto e = t->next.find(keys[i]); if (auto e = t->next.find(keys[i]); e != t->next.end()) {
e != t->next.end()) {
t = &e->second; t = &e->second;
} else { } else {
t = nullptr; t = nullptr;
@ -104,8 +103,7 @@ struct trie
} }
if (t) { if (t) {
append(t->search( append(t->search(keys.mid(2), limit(), max_edit_distance));
keys.mid(2), limit(), max_edit_distance));
} }
} }
@ -149,8 +147,7 @@ struct trie
class CompletionProxyModel : public QAbstractProxyModel class CompletionProxyModel : public QAbstractProxyModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY( Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
public: public:
CompletionProxyModel(QAbstractItemModel *model, CompletionProxyModel(QAbstractItemModel *model,
int max_mistakes = 2, int max_mistakes = 2,

View file

@ -1,882 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "DeviceVerificationFlow.h"
#include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "Utils.h"
#include "timeline/TimelineModel.h"
#include <QDateTime>
#include <QTimer>
#include <iostream>
static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
namespace msgs = mtx::events::msg;
static mtx::events::msg::KeyVerificationMac
key_verification_mac(mtx::crypto::SAS *sas,
mtx::identifiers::User sender,
const std::string &senderDevice,
mtx::identifiers::User receiver,
const std::string &receiverDevice,
const std::string &transactionId,
std::map<std::string, std::string> keys);
DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
DeviceVerificationFlow::Type flow_type,
TimelineModel *model,
QString userID,
QString deviceId_)
: sender(false)
, type(flow_type)
, deviceId(deviceId_)
, model_(model)
{
timeout = new QTimer(this);
timeout->setSingleShot(true);
this->sas = olm::client()->sas_init();
this->isMacVerified = false;
auto user_id = userID.toStdString();
this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id);
cache::client()->query_keys(
user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {},{}",
mtx::errors::to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code));
return;
}
if (!this->deviceId.isEmpty() &&
(res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
nhlog::net()->warn("no devices retrieved {}", user_id);
return;
}
this->their_keys = res;
});
cache::client()->query_keys(
http::client()->user_id().to_string(),
[this](const UserKeyCache &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {},{}",
mtx::errors::to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code));
return;
}
if (res.master_keys.keys.empty())
return;
if (auto status =
cache::verificationStatus(http::client()->user_id().to_string());
status && status->user_verified == crypto::Trust::Verified)
this->our_trusted_master_key = res.master_keys.keys.begin()->second;
});
if (model) {
connect(this->model_,
&TimelineModel::updateFlowEventId,
this,
[this](std::string event_id_) {
this->relation.rel_type = mtx::common::RelationType::Reference;
this->relation.event_id = event_id_;
this->transaction_id = event_id_;
});
}
connect(timeout, &QTimer::timeout, this, [this]() {
nhlog::crypto()->info("verification: timeout");
if (state_ != Success && state_ != Failed)
this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationStart,
this,
&DeviceVerificationFlow::handleStartMessage);
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationAccept,
this,
[this](const mtx::events::msg::KeyVerificationAccept &msg) {
nhlog::crypto()->info("verification: received accept");
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
}
if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
(msg.hash == "sha256") &&
(msg.message_authentication_code == "hkdf-hmac-sha256")) {
this->commitment = msg.commitment;
if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Emoji) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Emoji;
} else {
this->method = mtx::events::msg::SASMethods::Decimal;
}
this->mac_method = msg.message_authentication_code;
this->sendVerificationKey();
} else {
this->cancelVerification(
DeviceVerificationFlow::Error::UnknownMethod);
}
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationCancel,
this,
[this](const mtx::events::msg::KeyVerificationCancel &msg) {
nhlog::crypto()->info("verification: received cancel");
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
}
error_ = User;
emit errorChanged();
setState(Failed);
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationKey,
this,
[this](const mtx::events::msg::KeyVerificationKey &msg) {
nhlog::crypto()->info("verification: received key");
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
}
if (sender) {
if (state_ != WaitingForOtherToAccept) {
this->cancelVerification(OutOfOrder);
return;
}
} else {
if (state_ != WaitingForKeys) {
this->cancelVerification(OutOfOrder);
return;
}
}
this->sas->set_their_key(msg.key);
std::string info;
if (this->sender == true) {
info = "MATRIX_KEY_VERIFICATION_SAS|" +
http::client()->user_id().to_string() + "|" +
http::client()->device_id() + "|" + this->sas->public_key() +
"|" + this->toClient.to_string() + "|" +
this->deviceId.toStdString() + "|" + msg.key + "|" +
this->transaction_id;
} else {
info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
"|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
http::client()->user_id().to_string() + "|" +
http::client()->device_id() + "|" + this->sas->public_key() +
"|" + this->transaction_id;
}
nhlog::ui()->info("Info is: '{}'", info);
if (this->sender == false) {
this->sendVerificationKey();
} else {
if (this->commitment !=
mtx::crypto::bin2base64_unpadded(
mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
this->cancelVerification(
DeviceVerificationFlow::Error::MismatchedCommitment);
return;
}
}
if (this->method == mtx::events::msg::SASMethods::Emoji) {
this->sasList = this->sas->generate_bytes_emoji(info);
setState(CompareEmoji);
} else if (this->method == mtx::events::msg::SASMethods::Decimal) {
this->sasList = this->sas->generate_bytes_decimal(info);
setState(CompareNumber);
}
});
connect(
ChatPage::instance(),
&ChatPage::receivedDeviceVerificationMac,
this,
[this](const mtx::events::msg::KeyVerificationMac &msg) {
nhlog::crypto()->info("verification: received mac");
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
}
std::map<std::string, std::string> key_list;
std::string key_string;
for (const auto &mac : msg.mac) {
for (const auto &[deviceid, key] : their_keys.device_keys) {
(void)deviceid;
if (key.keys.count(mac.first))
key_list[mac.first] = key.keys.at(mac.first);
}
if (their_keys.master_keys.keys.count(mac.first))
key_list[mac.first] = their_keys.master_keys.keys[mac.first];
if (their_keys.user_signing_keys.keys.count(mac.first))
key_list[mac.first] =
their_keys.user_signing_keys.keys[mac.first];
if (their_keys.self_signing_keys.keys.count(mac.first))
key_list[mac.first] =
their_keys.self_signing_keys.keys[mac.first];
}
auto macs = key_verification_mac(sas.get(),
toClient,
this->deviceId.toStdString(),
http::client()->user_id(),
http::client()->device_id(),
this->transaction_id,
key_list);
for (const auto &[key, mac] : macs.mac) {
if (mac != msg.mac.at(key)) {
this->cancelVerification(
DeviceVerificationFlow::Error::KeyMismatch);
return;
}
}
if (msg.keys == macs.keys) {
mtx::requests::KeySignaturesUpload req;
if (utils::localUser().toStdString() == this->toClient.to_string()) {
// self verification, sign master key with device key, if we
// verified it
for (const auto &mac : msg.mac) {
if (their_keys.master_keys.keys.count(mac.first)) {
json j = their_keys.master_keys;
j.erase("signatures");
j.erase("unsigned");
mtx::crypto::CrossSigningKeys master_key = j;
master_key
.signatures[utils::localUser().toStdString()]
["ed25519:" +
http::client()->device_id()] =
olm::client()->sign_message(j.dump());
req.signatures[utils::localUser().toStdString()]
[master_key.keys.at(mac.first)] =
master_key;
} else if (mac.first ==
"ed25519:" + this->deviceId.toStdString()) {
// Sign their device key with self signing key
auto device_id = this->deviceId.toStdString();
if (their_keys.device_keys.count(device_id)) {
json j =
their_keys.device_keys.at(device_id);
j.erase("signatures");
j.erase("unsigned");
auto secret = cache::secret(
mtx::secret_storage::secrets::
cross_signing_self_signing);
if (!secret)
continue;
auto ssk =
mtx::crypto::PkSigning::from_seed(
*secret);
mtx::crypto::DeviceKeys dev = j;
dev.signatures
[utils::localUser().toStdString()]
["ed25519:" + ssk.public_key()] =
ssk.sign(j.dump());
req.signatures[utils::localUser()
.toStdString()]
[device_id] = dev;
}
}
}
} else {
// Sign their master key with user signing key
for (const auto &mac : msg.mac) {
if (their_keys.master_keys.keys.count(mac.first)) {
json j = their_keys.master_keys;
j.erase("signatures");
j.erase("unsigned");
auto secret =
cache::secret(mtx::secret_storage::secrets::
cross_signing_user_signing);
if (!secret)
continue;
auto usk =
mtx::crypto::PkSigning::from_seed(*secret);
mtx::crypto::CrossSigningKeys master_key = j;
master_key
.signatures[utils::localUser().toStdString()]
["ed25519:" + usk.public_key()] =
usk.sign(j.dump());
req.signatures[toClient.to_string()]
[master_key.keys.at(mac.first)] =
master_key;
}
}
}
if (!req.signatures.empty()) {
http::client()->keys_signatures_upload(
req,
[](const mtx::responses::KeySignaturesUpload &res,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"failed to upload signatures: {},{}",
mtx::errors::to_string(
err->matrix_error.errcode),
static_cast<int>(err->status_code));
}
for (const auto &[user_id, tmp] : res.errors)
for (const auto &[key_id, e] : tmp)
nhlog::net()->error(
"signature error for user {} and key "
"id {}: {}, {}",
user_id,
key_id,
mtx::errors::to_string(e.errcode),
e.error);
});
}
this->isMacVerified = true;
this->acceptDevice();
} else {
this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
}
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationReady,
this,
[this](const mtx::events::msg::KeyVerificationReady &msg) {
nhlog::crypto()->info("verification: received ready");
if (!sender) {
if (msg.from_device != http::client()->device_id()) {
error_ = User;
emit errorChanged();
setState(Failed);
}
return;
}
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
else {
this->deviceId = QString::fromStdString(msg.from_device);
}
}
this->startVerificationRequest();
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationDone,
this,
[this](const mtx::events::msg::KeyVerificationDone &msg) {
nhlog::crypto()->info("verification: receoved done");
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
}
nhlog::ui()->info("Flow done on other side");
});
timeout->start(TIMEOUT);
}
QString
DeviceVerificationFlow::state()
{
switch (state_) {
case PromptStartVerification:
return "PromptStartVerification";
case CompareEmoji:
return "CompareEmoji";
case CompareNumber:
return "CompareNumber";
case WaitingForKeys:
return "WaitingForKeys";
case WaitingForOtherToAccept:
return "WaitingForOtherToAccept";
case WaitingForMac:
return "WaitingForMac";
case Success:
return "Success";
case Failed:
return "Failed";
default:
return "";
}
}
void
DeviceVerificationFlow::next()
{
if (sender) {
switch (state_) {
case PromptStartVerification:
sendVerificationRequest();
break;
case CompareEmoji:
case CompareNumber:
sendVerificationMac();
break;
case WaitingForKeys:
case WaitingForOtherToAccept:
case WaitingForMac:
case Success:
case Failed:
nhlog::db()->error("verification: Invalid state transition!");
break;
}
} else {
switch (state_) {
case PromptStartVerification:
if (canonical_json.is_null())
sendVerificationReady();
else // legacy path without request and ready
acceptVerificationRequest();
break;
case CompareEmoji:
[[fallthrough]];
case CompareNumber:
sendVerificationMac();
break;
case WaitingForKeys:
case WaitingForOtherToAccept:
case WaitingForMac:
case Success:
case Failed:
nhlog::db()->error("verification: Invalid state transition!");
break;
}
}
}
QString
DeviceVerificationFlow::getUserId()
{
return QString::fromStdString(this->toClient.to_string());
}
QString
DeviceVerificationFlow::getDeviceId()
{
return this->deviceId;
}
bool
DeviceVerificationFlow::getSender()
{
return this->sender;
}
std::vector<int>
DeviceVerificationFlow::getSasList()
{
return this->sasList;
}
bool
DeviceVerificationFlow::isSelfVerification() const
{
return this->toClient.to_string() == http::client()->user_id().to_string();
}
void
DeviceVerificationFlow::setEventId(std::string event_id_)
{
this->relation.rel_type = mtx::common::RelationType::Reference;
this->relation.event_id = event_id_;
this->transaction_id = event_id_;
}
void
DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
std::string)
{
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relations.references()) {
if (msg.relations.references() != this->relation.event_id)
return;
}
if ((std::find(msg.key_agreement_protocols.begin(),
msg.key_agreement_protocols.end(),
"curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
(std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
(std::find(msg.message_authentication_codes.begin(),
msg.message_authentication_codes.end(),
"hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Emoji) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Emoji;
} else if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Decimal) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Decimal;
} else {
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
return;
}
if (!sender)
this->canonical_json = nlohmann::json(msg);
else {
if (utils::localUser().toStdString() < this->toClient.to_string()) {
this->canonical_json = nlohmann::json(msg);
}
}
if (state_ != PromptStartVerification)
this->acceptVerificationRequest();
} else {
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
}
}
//! accepts a verification
void
DeviceVerificationFlow::acceptVerificationRequest()
{
mtx::events::msg::KeyVerificationAccept req;
req.method = mtx::events::msg::VerificationMethods::SASv1;
req.key_agreement_protocol = "curve25519-hkdf-sha256";
req.hash = "sha256";
req.message_authentication_code = "hkdf-hmac-sha256";
if (this->method == mtx::events::msg::SASMethods::Emoji)
req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
else if (this->method == mtx::events::msg::SASMethods::Decimal)
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
req.commitment = mtx::crypto::bin2base64_unpadded(
mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
send(req);
setState(WaitingForKeys);
}
//! responds verification request
void
DeviceVerificationFlow::sendVerificationReady()
{
mtx::events::msg::KeyVerificationReady req;
req.from_device = http::client()->device_id();
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
send(req);
setState(WaitingForKeys);
}
//! accepts a verification
void
DeviceVerificationFlow::sendVerificationDone()
{
mtx::events::msg::KeyVerificationDone req;
send(req);
}
//! starts the verification flow
void
DeviceVerificationFlow::startVerificationRequest()
{
mtx::events::msg::KeyVerificationStart req;
req.from_device = http::client()->device_id();
req.method = mtx::events::msg::VerificationMethods::SASv1;
req.key_agreement_protocols = {"curve25519-hkdf-sha256"};
req.hashes = {"sha256"};
req.message_authentication_codes = {"hkdf-hmac-sha256"};
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal,
mtx::events::msg::SASMethods::Emoji};
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
req.transaction_id = this->transaction_id;
this->canonical_json = nlohmann::json(req);
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
req.relations.relations.push_back(this->relation);
// Set synthesized to surpress the nheko relation extensions
req.relations.synthesized = true;
this->canonical_json = nlohmann::json(req);
}
send(req);
setState(WaitingForOtherToAccept);
}
//! sends a verification request
void
DeviceVerificationFlow::sendVerificationRequest()
{
mtx::events::msg::KeyVerificationRequest req;
req.from_device = http::client()->device_id();
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
QDateTime currentTime = QDateTime::currentDateTimeUtc();
req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
req.to = this->toClient.to_string();
req.msgtype = "m.key.verification.request";
req.body = "User is requesting to verify keys with you. However, your client does "
"not support this method, so you will need to use the legacy method of "
"key verification.";
}
send(req);
setState(WaitingForOtherToAccept);
}
//! cancels a verification flow
void
DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
{
if (state_ == State::Success || state_ == State::Failed)
return;
mtx::events::msg::KeyVerificationCancel req;
if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
req.code = "m.unknown_method";
req.reason = "unknown method received";
} else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
req.code = "m.mismatched_commitment";
req.reason = "commitment didn't match";
} else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
req.code = "m.mismatched_sas";
req.reason = "sas didn't match";
} else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
req.code = "m.key_match";
req.reason = "keys did not match";
} else if (error_code == DeviceVerificationFlow::Error::Timeout) {
req.code = "m.timeout";
req.reason = "timed out";
} else if (error_code == DeviceVerificationFlow::Error::User) {
req.code = "m.user";
req.reason = "user cancelled the verification";
} else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
req.code = "m.unexpected_message";
req.reason = "received messages out of order";
}
this->error_ = error_code;
emit errorChanged();
this->setState(Failed);
send(req);
}
//! sends the verification key
void
DeviceVerificationFlow::sendVerificationKey()
{
mtx::events::msg::KeyVerificationKey req;
req.key = this->sas->public_key();
send(req);
}
mtx::events::msg::KeyVerificationMac
key_verification_mac(mtx::crypto::SAS *sas,
mtx::identifiers::User sender,
const std::string &senderDevice,
mtx::identifiers::User receiver,
const std::string &receiverDevice,
const std::string &transactionId,
std::map<std::string, std::string> keys)
{
mtx::events::msg::KeyVerificationMac req;
std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
receiver.to_string() + receiverDevice + transactionId;
std::string key_list;
bool first = true;
for (const auto &[key_id, key] : keys) {
req.mac[key_id] = sas->calculate_mac(key, info + key_id);
if (!first)
key_list += ",";
key_list += key_id;
first = false;
}
req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
return req;
}
//! sends the mac of the keys
void
DeviceVerificationFlow::sendVerificationMac()
{
std::map<std::string, std::string> key_list;
key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
// send our master key, if we trust it
if (!this->our_trusted_master_key.empty())
key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
mtx::events::msg::KeyVerificationMac req =
key_verification_mac(sas.get(),
http::client()->user_id(),
http::client()->device_id(),
this->toClient,
this->deviceId.toStdString(),
this->transaction_id,
key_list);
send(req);
setState(WaitingForMac);
acceptDevice();
}
//! Completes the verification flow
void
DeviceVerificationFlow::acceptDevice()
{
if (!isMacVerified) {
setState(WaitingForMac);
} else if (state_ == WaitingForMac) {
cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
this->sendVerificationDone();
setState(Success);
// Request secrets. We should probably check somehow, if a device knowns about the
// secrets.
if (utils::localUser().toStdString() == this->toClient.to_string() &&
(!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) ||
!cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) {
olm::request_cross_signing_keys();
}
}
}
void
DeviceVerificationFlow::unverify()
{
cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
emit refreshProfile();
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
TimelineModel *timelineModel_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString event_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_,
Type::RoomMsg,
timelineModel_,
other_user_,
QString::fromStdString(msg.from_device)));
flow->setEventId(event_id_.toStdString());
if (std::find(msg.methods.begin(),
msg.methods.end(),
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
flow->cancelVerification(UnknownMethod);
}
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString txn_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
flow->transaction_id = txn_id_.toStdString();
if (std::find(msg.methods.begin(),
msg.methods.end(),
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
flow->cancelVerification(UnknownMethod);
}
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
const mtx::events::msg::KeyVerificationStart &msg,
QString other_user_,
QString txn_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
flow->transaction_id = txn_id_.toStdString();
flow->handleStartMessage(msg, "");
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
TimelineModel *timelineModel_,
QString userid)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
flow->sender = true;
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
flow->sender = true;
flow->transaction_id = http::client()->generate_txn_id();
return flow;
}

View file

@ -1,251 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <mtx/responses/crypto.hpp>
#include <nlohmann/json.hpp>
#include "CacheCryptoStructs.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Olm.h"
#include "timeline/TimelineModel.h"
class QTimer;
using sas_ptr = std::unique_ptr<mtx::crypto::SAS>;
// clang-format off
/*
* Stolen from fluffy chat :D
*
* State | +-------------+ +-----------+ |
* | | AliceDevice | | BobDevice | |
* | | (sender) | | | |
* | +-------------+ +-----------+ |
* promptStartVerify | | | |
* | o | (m.key.verification.request) | |
* | p |-------------------------------->| (ASK FOR VERIFICATION REQUEST) |
* waitForOtherAccept | t | | | promptStartVerify
* && | i | (m.key.verification.ready) | |
* no commitment | o |<--------------------------------| |
* && | n | | |
* no canonical_json | a | (m.key.verification.start) | | waitingForKeys
* | l |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
* | | | | && no canonical_json
* | | m.key.verification.start | |
* waitForOtherAccept | |-------------------------------->| (IF NOT ALREADY ASKED, |
* && | | | ASK FOR VERIFICATION REQUEST) | promptStartVerify, if not accepted
* canonical_json | | m.key.verification.accept | |
* | |<--------------------------------| |
* waitForOtherAccept | | | | waitingForKeys
* && | | m.key.verification.key | | && canonical_json
* commitment | |-------------------------------->| | && commitment
* | | | |
* | | m.key.verification.key | |
* | |<--------------------------------| |
* compareEmoji/Number| | | | compareEmoji/Number
* | | COMPARE EMOJI / NUMBERS | |
* | | | |
* waitingForMac | | m.key.verification.mac | | waitingForMac
* | success |<------------------------------->| success |
* | | | |
* success/fail | | m.key.verification.done | | success/fail
* | |<------------------------------->| |
*/
// clang-format on
class DeviceVerificationFlow : public QObject
{
Q_OBJECT
Q_PROPERTY(QString state READ state NOTIFY stateChanged)
Q_PROPERTY(Error error READ error NOTIFY errorChanged)
Q_PROPERTY(QString userId READ getUserId CONSTANT)
Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
Q_PROPERTY(bool sender READ getSender CONSTANT)
Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT)
Q_PROPERTY(bool isDeviceVerification READ isDeviceVerification CONSTANT)
Q_PROPERTY(bool isSelfVerification READ isSelfVerification CONSTANT)
public:
enum State
{
PromptStartVerification,
WaitingForOtherToAccept,
WaitingForKeys,
CompareEmoji,
CompareNumber,
WaitingForMac,
Success,
Failed,
};
Q_ENUM(State)
enum Type
{
ToDevice,
RoomMsg
};
enum Error
{
UnknownMethod,
MismatchedCommitment,
MismatchedSAS,
KeyMismatch,
Timeout,
User,
OutOfOrder,
};
Q_ENUM(Error)
static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification(
QObject *parent_,
TimelineModel *timelineModel_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString event_id_);
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
QObject *parent_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString txn_id_);
static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification(
QObject *parent_,
const mtx::events::msg::KeyVerificationStart &msg,
QString other_user_,
QString txn_id_);
static QSharedPointer<DeviceVerificationFlow>
InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent,
QString userid,
QString device);
// getters
QString state();
Error error() { return error_; }
QString getUserId();
QString getDeviceId();
bool getSender();
std::vector<int> getSasList();
QString transactionId() { return QString::fromStdString(this->transaction_id); }
// setters
void setDeviceId(QString deviceID);
void setEventId(std::string event_id);
bool isDeviceVerification() const
{
return this->type == DeviceVerificationFlow::Type::ToDevice;
}
bool isSelfVerification() const;
void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
public slots:
//! unverifies a device
void unverify();
//! Continues the flow
void next();
//! Cancel the flow
void cancel() { cancelVerification(User); }
signals:
void refreshProfile();
void stateChanged();
void errorChanged();
private:
DeviceVerificationFlow(QObject *,
DeviceVerificationFlow::Type flow_type,
TimelineModel *model,
QString userID,
QString deviceId_);
void setState(State state)
{
if (state != state_) {
state_ = state;
emit stateChanged();
}
}
void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
//! sends a verification request
void sendVerificationRequest();
//! accepts a verification request
void sendVerificationReady();
//! completes the verification flow();
void sendVerificationDone();
//! accepts a verification
void acceptVerificationRequest();
//! starts the verification flow
void startVerificationRequest();
//! cancels a verification flow
void cancelVerification(DeviceVerificationFlow::Error error_code);
//! sends the verification key
void sendVerificationKey();
//! sends the mac of the keys
void sendVerificationMac();
//! Completes the verification flow
void acceptDevice();
std::string transaction_id;
bool sender;
Type type;
mtx::identifiers::User toClient;
QString deviceId;
// public part of our master key, when trusted or empty
std::string our_trusted_master_key;
mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
QTimer *timeout = nullptr;
sas_ptr sas;
std::string mac_method;
std::string commitment;
nlohmann::json canonical_json;
std::vector<int> sasList;
UserKeyCache their_keys;
TimelineModel *model_;
mtx::common::Relation relation;
State state_ = PromptStartVerification;
Error error_ = UnknownMethod;
bool isMacVerified = false;
template<typename T>
void send(T msg)
{
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
mtx::requests::ToDeviceMessages<T> body;
msg.transaction_id = this->transaction_id;
body[this->toClient][deviceId.toStdString()] = msg;
http::client()->send_to_device<T>(
this->transaction_id, body, [](mtx::http::RequestErr err) {
if (err)
nhlog::net()->warn(
"failed to send verification to_device message: {} {}",
err->matrix_error.error,
static_cast<int>(err->status_code));
});
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
if constexpr (!std::is_same_v<T,
mtx::events::msg::KeyVerificationRequest>) {
msg.relations.relations.push_back(this->relation);
// Set synthesized to surpress the nheko relation extensions
msg.relations.synthesized = true;
}
(model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
}
nhlog::net()->debug(
"Sent verification step: {} in state: {}",
mtx::events::to_string(mtx::events::to_device_content_to_type<T>),
state().toStdString());
}
};

View file

@ -39,8 +39,7 @@ struct EventMsgType
if constexpr (std::is_same_v<std::optional<std::string>, if constexpr (std::is_same_v<std::optional<std::string>,
std::remove_cv_t<decltype(e.content.msgtype)>>) std::remove_cv_t<decltype(e.content.msgtype)>>)
return mtx::events::getMessageType(e.content.msgtype.value()); return mtx::events::getMessageType(e.content.msgtype.value());
else if constexpr (std::is_same_v< else if constexpr (std::is_same_v<std::string,
std::string,
std::remove_cv_t<decltype(e.content.msgtype)>>) std::remove_cv_t<decltype(e.content.msgtype)>>)
return mtx::events::getMessageType(e.content.msgtype); return mtx::events::getMessageType(e.content.msgtype);
} }
@ -75,8 +74,7 @@ struct CallType
template<class T> template<class T>
std::string operator()(const T &e) std::string operator()(const T &e)
{ {
if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, T>) {
T>) {
const char video[] = "m=video"; const char video[] = "m=video";
const std::string &sdp = e.content.sdp; const std::string &sdp = e.content.sdp;
return std::search(sdp.cbegin(), return std::search(sdp.cbegin(),
@ -104,8 +102,7 @@ struct EventBody
if constexpr (std::is_same_v<std::optional<std::string>, if constexpr (std::is_same_v<std::optional<std::string>,
std::remove_cv_t<decltype(e.content.body)>>) std::remove_cv_t<decltype(e.content.body)>>)
return e.content.body ? e.content.body.value() : ""; return e.content.body ? e.content.body.value() : "";
else if constexpr (std::is_same_v< else if constexpr (std::is_same_v<std::string,
std::string,
std::remove_cv_t<decltype(e.content.body)>>) std::remove_cv_t<decltype(e.content.body)>>)
return e.content.body; return e.content.body;
} }

View file

@ -16,8 +16,7 @@ ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *paren
auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt); auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt);
for (const auto &pack : packs_) { for (const auto &pack : packs_) {
packs.push_back( packs.push_back(QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
} }
} }

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