mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 11:00:48 +03:00
Update video_player_enhancements with changes from master
This commit is contained in:
commit
743a83c8e6
265 changed files with 53777 additions and 34019 deletions
73
.ci/macos/notarize.sh
Executable file
73
.ci/macos/notarize.sh
Executable 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
|
|
@ -1,14 +1,14 @@
|
|||
---
|
||||
Language: Cpp
|
||||
Standard: Cpp11
|
||||
AccessModifierOffset: -8
|
||||
Standard: c++17
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: true
|
||||
AllowShortFunctionsOnASingleLine: true
|
||||
BasedOnStyle: Mozilla
|
||||
ColumnLimit: 100
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 8
|
||||
IndentWidth: 4
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
PointerAlignment: Right
|
||||
Cpp11BracedListStyle: true
|
||||
|
|
|
@ -55,7 +55,6 @@ build-macos:
|
|||
#- brew update
|
||||
#- brew reinstall --force python3
|
||||
#- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
|
||||
- pip3 install dmgbuild
|
||||
- rm -rf ../.hunter && mv .hunter ../.hunter || true
|
||||
script:
|
||||
- export PATH=/usr/local/opt/qt@5/bin/:${PATH}
|
||||
|
@ -72,19 +71,40 @@ build-macos:
|
|||
- cmake --build build
|
||||
after_script:
|
||||
- mv ../.hunter .hunter
|
||||
- ./.ci/macos/deploy.sh
|
||||
- ./.ci/upload-nightly-gitlab.sh artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
|
||||
name: nheko-${CI_COMMIT_SHORT_SHA}-macos
|
||||
expose_as: 'macos-dmg'
|
||||
- build/nheko.app
|
||||
name: nheko-${CI_COMMIT_SHORT_SHA}-macos-app
|
||||
expose_as: 'macos-app'
|
||||
public: false
|
||||
cache:
|
||||
key: "${CI_JOB_NAME}"
|
||||
paths:
|
||||
- .hunter/
|
||||
- "${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:
|
||||
stage: build
|
||||
image: ubuntu:latest
|
||||
|
@ -171,7 +191,7 @@ appimage-amd64:
|
|||
- apt-get install -y git wget curl
|
||||
|
||||
# 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
|
||||
- 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
|
||||
|
|
|
@ -281,8 +281,6 @@ set(SRC_FILES
|
|||
src/dialogs/CreateRoom.cpp
|
||||
src/dialogs/FallbackAuth.cpp
|
||||
src/dialogs/ImageOverlay.cpp
|
||||
src/dialogs/JoinRoom.cpp
|
||||
src/dialogs/LeaveRoom.cpp
|
||||
src/dialogs/Logout.cpp
|
||||
src/dialogs/PreviewUploadOverlay.cpp
|
||||
src/dialogs/ReCaptcha.cpp
|
||||
|
@ -311,6 +309,8 @@ set(SRC_FILES
|
|||
src/ui/InfoMessage.cpp
|
||||
src/ui/Label.cpp
|
||||
src/ui/LoadingIndicator.cpp
|
||||
src/ui/MxcAnimatedImage.cpp
|
||||
src/ui/MxcMediaProxy.cpp
|
||||
src/ui/NhekoCursorShape.cpp
|
||||
src/ui/NhekoDropArea.cpp
|
||||
src/ui/NhekoGlobalObject.cpp
|
||||
|
@ -326,30 +326,37 @@ set(SRC_FILES
|
|||
src/ui/Theme.cpp
|
||||
src/ui/ThemeManager.cpp
|
||||
src/ui/ToggleButton.cpp
|
||||
src/ui/UIA.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
|
||||
src/notifications/Manager.cpp
|
||||
|
||||
src/AvatarProvider.cpp
|
||||
src/BlurhashProvider.cpp
|
||||
src/Cache.cpp
|
||||
src/CallDevices.cpp
|
||||
src/CallManager.cpp
|
||||
src/ChatPage.cpp
|
||||
src/Clipboard.cpp
|
||||
src/ColorImageProvider.cpp
|
||||
src/CompletionProxyModel.cpp
|
||||
src/DeviceVerificationFlow.cpp
|
||||
src/EventAccessors.cpp
|
||||
src/InviteesModel.cpp
|
||||
src/JdenticonProvider.cpp
|
||||
src/Logging.cpp
|
||||
src/LoginPage.cpp
|
||||
src/MainWindow.cpp
|
||||
src/MatrixClient.cpp
|
||||
src/MemberList.cpp
|
||||
src/MxcImageProvider.cpp
|
||||
src/Olm.cpp
|
||||
src/ReadReceiptsModel.cpp
|
||||
src/RegisterPage.cpp
|
||||
src/SSOHandler.cpp
|
||||
|
@ -359,9 +366,9 @@ set(SRC_FILES
|
|||
src/TrayIcon.cpp
|
||||
src/UserSettingsPage.cpp
|
||||
src/UsersModel.cpp
|
||||
src/RoomDirectoryModel.cpp
|
||||
src/RoomsModel.cpp
|
||||
src/Utils.cpp
|
||||
src/WebRTCSession.cpp
|
||||
src/WelcomePage.cpp
|
||||
src/main.cpp
|
||||
|
||||
|
@ -381,7 +388,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||
FetchContent_Declare(
|
||||
MatrixClient
|
||||
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_TESTS OFF CACHE INTERNAL "")
|
||||
|
@ -492,8 +499,6 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/dialogs/CreateRoom.h
|
||||
src/dialogs/FallbackAuth.h
|
||||
src/dialogs/ImageOverlay.h
|
||||
src/dialogs/JoinRoom.h
|
||||
src/dialogs/LeaveRoom.h
|
||||
src/dialogs/Logout.h
|
||||
src/dialogs/PreviewUploadOverlay.h
|
||||
src/dialogs/ReCaptcha.h
|
||||
|
@ -520,6 +525,8 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/ui/InfoMessage.h
|
||||
src/ui/Label.h
|
||||
src/ui/LoadingIndicator.h
|
||||
src/ui/MxcAnimatedImage.h
|
||||
src/ui/MxcMediaProxy.h
|
||||
src/ui/Menu.h
|
||||
src/ui/NhekoCursorShape.h
|
||||
src/ui/NhekoDropArea.h
|
||||
|
@ -535,28 +542,35 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/ui/Theme.h
|
||||
src/ui/ThemeManager.h
|
||||
src/ui/ToggleButton.h
|
||||
src/ui/UIA.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/AvatarProvider.h
|
||||
src/BlurhashProvider.h
|
||||
src/CacheCryptoStructs.h
|
||||
src/Cache_p.h
|
||||
src/CallDevices.h
|
||||
src/CallManager.h
|
||||
src/ChatPage.h
|
||||
src/Clipboard.h
|
||||
src/CombinedImagePackModel.h
|
||||
src/CompletionProxyModel.h
|
||||
src/DeviceVerificationFlow.h
|
||||
src/ImagePackListModel.h
|
||||
src/InviteesModel.h
|
||||
src/JdenticonProvider.h
|
||||
src/LoginPage.h
|
||||
src/MainWindow.h
|
||||
src/MemberList.h
|
||||
src/MxcImageProvider.h
|
||||
src/Olm.h
|
||||
src/RegisterPage.h
|
||||
src/RoomsModel.h
|
||||
src/SSOHandler.h
|
||||
|
@ -564,7 +578,8 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/TrayIcon.h
|
||||
src/UserSettingsPage.h
|
||||
src/UsersModel.h
|
||||
src/WebRTCSession.h
|
||||
src/RoomDirectoryModel.h
|
||||
src/RoomsModel.h
|
||||
src/WelcomePage.h
|
||||
src/ReadReceiptsModel.h
|
||||
)
|
||||
|
@ -576,7 +591,7 @@ include(Translations)
|
|||
set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
|
||||
|
||||
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)
|
||||
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)
|
||||
|
|
27
README.md
27
README.md
|
@ -77,6 +77,7 @@ sudo dnf install nheko
|
|||
#### Gentoo Linux
|
||||
```bash
|
||||
sudo eselect repository enable guru
|
||||
sudo emaint sync -r guru
|
||||
sudo emerge -a nheko
|
||||
```
|
||||
|
||||
|
@ -126,11 +127,31 @@ choco install nheko-reborn
|
|||
|
||||
### FAQ
|
||||
|
||||
##
|
||||
---
|
||||
|
||||
**Q:** Why don't videos run for me on Windows?
|
||||
|
||||
**A:** You're probably missing the required video codecs, download [K-Lite Codec Pack](https://codecguide.com/download_kl.htm).
|
||||
##
|
||||
|
||||
---
|
||||
|
||||
**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
|
||||
|
||||
|
@ -150,7 +171,7 @@ choco install nheko-reborn
|
|||
- Voice call support: dtls, opus, rtpmanager, srtp, webrtc
|
||||
- Video call support (optional): compositor, opengl, qmlgl, rtp, vpx
|
||||
- [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:
|
||||
- Clang 6 (tested on Travis CI)
|
||||
- GCC 7 (tested on Travis CI)
|
||||
|
|
|
@ -163,7 +163,7 @@ modules:
|
|||
buildsystem: cmake-ninja
|
||||
name: mtxclient
|
||||
sources:
|
||||
- commit: deb51ef1d6df870098069312f0a1999550e1eb85
|
||||
- commit: 7fe7a70fcf7540beb6d7b4847e53a425de66c6bf
|
||||
type: git
|
||||
url: https://github.com/Nheko-Reborn/mtxclient.git
|
||||
- config-opts:
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# emoji-test.txt
|
||||
# Date: 2020-09-12, 22:19:50 GMT
|
||||
# © 2020 Unicode®, Inc.
|
||||
# Date: 2021-08-26, 17:22:23 GMT
|
||||
# © 2021 Unicode®, Inc.
|
||||
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
|
||||
# For terms of use, see http://www.unicode.org/terms_of_use.html
|
||||
#
|
||||
# Emoji Keyboard/Display Test Data for UTS #51
|
||||
# Version: 13.1
|
||||
# Version: 14.0
|
||||
#
|
||||
# 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
|
||||
1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face
|
||||
1F643 ; fully-qualified # 🙃 E1.0 upside-down face
|
||||
1FAE0 ; fully-qualified # 🫠 E14.0 melting face
|
||||
1F609 ; fully-qualified # 😉 E0.6 winking face
|
||||
1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes
|
||||
1F607 ; fully-qualified # 😇 E1.0 smiling face with halo
|
||||
|
@ -68,10 +69,13 @@
|
|||
1F911 ; fully-qualified # 🤑 E1.0 money-mouth face
|
||||
|
||||
# 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
|
||||
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
|
||||
1F914 ; fully-qualified # 🤔 E1.0 thinking face
|
||||
1FAE1 ; fully-qualified # 🫡 E14.0 saluting face
|
||||
|
||||
# subgroup: face-neutral-skeptical
|
||||
1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face
|
||||
|
@ -79,6 +83,7 @@
|
|||
1F610 ; fully-qualified # 😐 E0.7 neutral face
|
||||
1F611 ; fully-qualified # 😑 E1.0 expressionless face
|
||||
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 ; minimally-qualified # 😶🌫 E13.1 face in clouds
|
||||
1F60F ; fully-qualified # 😏 E0.6 smirking face
|
||||
|
@ -105,7 +110,7 @@
|
|||
1F975 ; fully-qualified # 🥵 E11.0 hot face
|
||||
1F976 ; fully-qualified # 🥶 E11.0 cold face
|
||||
1F974 ; fully-qualified # 🥴 E11.0 woozy face
|
||||
1F635 ; fully-qualified # 😵 E0.6 knocked-out face
|
||||
1F635 ; fully-qualified # 😵 E0.6 face with crossed-out eyes
|
||||
1F635 200D 1F4AB ; fully-qualified # 😵💫 E13.1 face with spiral eyes
|
||||
1F92F ; fully-qualified # 🤯 E5.0 exploding head
|
||||
|
||||
|
@ -121,6 +126,7 @@
|
|||
|
||||
# subgroup: face-concerned
|
||||
1F615 ; fully-qualified # 😕 E1.0 confused face
|
||||
1FAE4 ; fully-qualified # 🫤 E14.0 face with diagonal mouth
|
||||
1F61F ; fully-qualified # 😟 E1.0 worried face
|
||||
1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face
|
||||
2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face
|
||||
|
@ -130,6 +136,7 @@
|
|||
1F632 ; fully-qualified # 😲 E0.6 astonished face
|
||||
1F633 ; fully-qualified # 😳 E0.6 flushed 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
|
||||
1F627 ; fully-qualified # 😧 E1.0 anguished face
|
||||
1F628 ; fully-qualified # 😨 E0.6 fearful face
|
||||
|
@ -232,8 +239,8 @@
|
|||
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
|
||||
1F4A4 ; fully-qualified # 💤 E0.6 zzz
|
||||
|
||||
# Smileys & Emotion subtotal: 170
|
||||
# Smileys & Emotion subtotal: 170 w/o modifiers
|
||||
# Smileys & Emotion subtotal: 177
|
||||
# Smileys & Emotion subtotal: 177 w/o modifiers
|
||||
|
||||
# group: People & Body
|
||||
|
||||
|
@ -269,6 +276,30 @@
|
|||
1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone
|
||||
1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
|
||||
1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone
|
||||
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
|
||||
1F44C ; fully-qualified # 👌 E0.6 OK hand
|
||||
|
@ -302,6 +333,12 @@
|
|||
1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone
|
||||
1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
|
||||
1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone
|
||||
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 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: 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 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-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
|
||||
1F44D ; fully-qualified # 👍 E0.6 thumbs up
|
||||
|
@ -411,6 +454,12 @@
|
|||
1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone
|
||||
1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone
|
||||
1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone
|
||||
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 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: 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 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
|
||||
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 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone
|
||||
1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone
|
||||
|
@ -501,6 +575,7 @@
|
|||
1F441 ; unqualified # 👁 E0.7 eye
|
||||
1F445 ; fully-qualified # 👅 E0.6 tongue
|
||||
1F444 ; fully-qualified # 👄 E0.6 mouth
|
||||
1FAE6 ; fully-qualified # 🫦 E14.0 biting lip
|
||||
|
||||
# subgroup: person
|
||||
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 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
|
||||
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 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: 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 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-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 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: 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 2640 FE0F ; fully-qualified # 🧟♀️ E5.0 woman zombie
|
||||
1F9DF 200D 2640 ; minimally-qualified # 🧟♀ E5.0 woman zombie
|
||||
1F9CC ; fully-qualified # 🧌 E14.0 troll
|
||||
|
||||
# subgroup: person-activity
|
||||
1F486 ; fully-qualified # 💆 E0.6 person getting massage
|
||||
|
@ -3168,8 +3262,8 @@
|
|||
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
|
||||
1F463 ; fully-qualified # 👣 E0.6 footprints
|
||||
|
||||
# People & Body subtotal: 2899
|
||||
# People & Body subtotal: 494 w/o modifiers
|
||||
# People & Body subtotal: 2986
|
||||
# People & Body subtotal: 506 w/o modifiers
|
||||
|
||||
# group: Component
|
||||
|
||||
|
@ -3304,6 +3398,7 @@
|
|||
1F988 ; fully-qualified # 🦈 E3.0 shark
|
||||
1F419 ; fully-qualified # 🐙 E0.6 octopus
|
||||
1F41A ; fully-qualified # 🐚 E0.6 spiral shell
|
||||
1FAB8 ; fully-qualified # 🪸 E14.0 coral
|
||||
|
||||
# subgroup: animal-bug
|
||||
1F40C ; fully-qualified # 🐌 E0.6 snail
|
||||
|
@ -3329,6 +3424,7 @@
|
|||
1F490 ; fully-qualified # 💐 E0.6 bouquet
|
||||
1F338 ; fully-qualified # 🌸 E0.6 cherry blossom
|
||||
1F4AE ; fully-qualified # 💮 E0.6 white flower
|
||||
1FAB7 ; fully-qualified # 🪷 E14.0 lotus
|
||||
1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette
|
||||
1F3F5 ; unqualified # 🏵 E0.7 rosette
|
||||
1F339 ; fully-qualified # 🌹 E0.6 rose
|
||||
|
@ -3353,9 +3449,11 @@
|
|||
1F341 ; fully-qualified # 🍁 E0.6 maple leaf
|
||||
1F342 ; fully-qualified # 🍂 E0.6 fallen leaf
|
||||
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: 147 w/o modifiers
|
||||
# Animals & Nature subtotal: 151
|
||||
# Animals & Nature subtotal: 151 w/o modifiers
|
||||
|
||||
# group: Food & Drink
|
||||
|
||||
|
@ -3396,6 +3494,7 @@
|
|||
1F9C5 ; fully-qualified # 🧅 E12.0 onion
|
||||
1F344 ; fully-qualified # 🍄 E0.6 mushroom
|
||||
1F95C ; fully-qualified # 🥜 E3.0 peanuts
|
||||
1FAD8 ; fully-qualified # 🫘 E14.0 beans
|
||||
1F330 ; fully-qualified # 🌰 E0.6 chestnut
|
||||
|
||||
# subgroup: food-prepared
|
||||
|
@ -3491,6 +3590,7 @@
|
|||
1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs
|
||||
1F942 ; fully-qualified # 🥂 E3.0 clinking glasses
|
||||
1F943 ; fully-qualified # 🥃 E3.0 tumbler glass
|
||||
1FAD7 ; fully-qualified # 🫗 E14.0 pouring liquid
|
||||
1F964 ; fully-qualified # 🥤 E5.0 cup with straw
|
||||
1F9CB ; fully-qualified # 🧋 E13.0 bubble tea
|
||||
1F9C3 ; fully-qualified # 🧃 E12.0 beverage box
|
||||
|
@ -3504,10 +3604,11 @@
|
|||
1F374 ; fully-qualified # 🍴 E0.6 fork and knife
|
||||
1F944 ; fully-qualified # 🥄 E3.0 spoon
|
||||
1F52A ; fully-qualified # 🔪 E0.6 kitchen knife
|
||||
1FAD9 ; fully-qualified # 🫙 E14.0 jar
|
||||
1F3FA ; fully-qualified # 🏺 E1.0 amphora
|
||||
|
||||
# Food & Drink subtotal: 131
|
||||
# Food & Drink subtotal: 131 w/o modifiers
|
||||
# Food & Drink subtotal: 134
|
||||
# Food & Drink subtotal: 134 w/o modifiers
|
||||
|
||||
# group: Travel & Places
|
||||
|
||||
|
@ -3597,6 +3698,7 @@
|
|||
2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs
|
||||
2668 ; unqualified # ♨ E0.6 hot springs
|
||||
1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse
|
||||
1F6DD ; fully-qualified # 🛝 E14.0 playground slide
|
||||
1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel
|
||||
1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster
|
||||
1F488 ; fully-qualified # 💈 E0.6 barber pole
|
||||
|
@ -3652,6 +3754,7 @@
|
|||
1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum
|
||||
1F6E2 ; unqualified # 🛢 E0.7 oil drum
|
||||
26FD ; fully-qualified # ⛽ E0.6 fuel pump
|
||||
1F6DE ; fully-qualified # 🛞 E14.0 wheel
|
||||
1F6A8 ; fully-qualified # 🚨 E0.6 police car light
|
||||
1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light
|
||||
1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light
|
||||
|
@ -3660,6 +3763,7 @@
|
|||
|
||||
# subgroup: transport-water
|
||||
2693 ; fully-qualified # ⚓ E0.6 anchor
|
||||
1F6DF ; fully-qualified # 🛟 E14.0 ring buoy
|
||||
26F5 ; fully-qualified # ⛵ E0.6 sailboat
|
||||
1F6F6 ; fully-qualified # 🛶 E3.0 canoe
|
||||
1F6A4 ; fully-qualified # 🚤 E0.6 speedboat
|
||||
|
@ -3797,8 +3901,8 @@
|
|||
1F4A7 ; fully-qualified # 💧 E0.6 droplet
|
||||
1F30A ; fully-qualified # 🌊 E0.6 water wave
|
||||
|
||||
# Travel & Places subtotal: 264
|
||||
# Travel & Places subtotal: 264 w/o modifiers
|
||||
# Travel & Places subtotal: 267
|
||||
# Travel & Places subtotal: 267 w/o modifiers
|
||||
|
||||
# group: Activities
|
||||
|
||||
|
@ -3874,6 +3978,7 @@
|
|||
1F52E ; fully-qualified # 🔮 E0.6 crystal ball
|
||||
1FA84 ; fully-qualified # 🪄 E13.0 magic wand
|
||||
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
|
||||
1FAAC ; fully-qualified # 🪬 E14.0 hamsa
|
||||
1F3AE ; fully-qualified # 🎮 E0.6 video game
|
||||
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
|
||||
1F579 ; unqualified # 🕹 E0.7 joystick
|
||||
|
@ -3882,6 +3987,7 @@
|
|||
1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece
|
||||
1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear
|
||||
1FA85 ; fully-qualified # 🪅 E13.0 piñata
|
||||
1FAA9 ; fully-qualified # 🪩 E14.0 mirror ball
|
||||
1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls
|
||||
2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit
|
||||
2660 ; unqualified # ♠ E0.6 spade suit
|
||||
|
@ -3907,8 +4013,8 @@
|
|||
1F9F6 ; fully-qualified # 🧶 E11.0 yarn
|
||||
1FAA2 ; fully-qualified # 🪢 E13.0 knot
|
||||
|
||||
# Activities subtotal: 95
|
||||
# Activities subtotal: 95 w/o modifiers
|
||||
# Activities subtotal: 97
|
||||
# Activities subtotal: 97 w/o modifiers
|
||||
|
||||
# group: Objects
|
||||
|
||||
|
@ -4009,6 +4115,7 @@
|
|||
|
||||
# subgroup: computer
|
||||
1F50B ; fully-qualified # 🔋 E0.6 battery
|
||||
1FAAB ; fully-qualified # 🪫 E14.0 low battery
|
||||
1F50C ; fully-qualified # 🔌 E0.6 electric plug
|
||||
1F4BB ; fully-qualified # 💻 E0.6 laptop
|
||||
1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer
|
||||
|
@ -4207,7 +4314,9 @@
|
|||
1FA78 ; fully-qualified # 🩸 E12.0 drop of blood
|
||||
1F48A ; fully-qualified # 💊 E0.6 pill
|
||||
1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage
|
||||
1FA7C ; fully-qualified # 🩼 E14.0 crutch
|
||||
1FA7A ; fully-qualified # 🩺 E12.0 stethoscope
|
||||
1FA7B ; fully-qualified # 🩻 E14.0 x-ray
|
||||
|
||||
# subgroup: household
|
||||
1F6AA ; fully-qualified # 🚪 E0.6 door
|
||||
|
@ -4232,6 +4341,7 @@
|
|||
1F9FB ; fully-qualified # 🧻 E11.0 roll of paper
|
||||
1FAA3 ; fully-qualified # 🪣 E13.0 bucket
|
||||
1F9FC ; fully-qualified # 🧼 E11.0 soap
|
||||
1FAE7 ; fully-qualified # 🫧 E14.0 bubbles
|
||||
1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush
|
||||
1F9FD ; fully-qualified # 🧽 E11.0 sponge
|
||||
1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher
|
||||
|
@ -4246,9 +4356,10 @@
|
|||
26B1 ; unqualified # ⚱ E1.0 funeral urn
|
||||
1F5FF ; fully-qualified # 🗿 E0.6 moai
|
||||
1FAA7 ; fully-qualified # 🪧 E13.0 placard
|
||||
1FAAA ; fully-qualified # 🪪 E14.0 identification card
|
||||
|
||||
# Objects subtotal: 299
|
||||
# Objects subtotal: 299 w/o modifiers
|
||||
# Objects subtotal: 304
|
||||
# Objects subtotal: 304 w/o modifiers
|
||||
|
||||
# group: Symbols
|
||||
|
||||
|
@ -4409,6 +4520,7 @@
|
|||
2795 ; fully-qualified # ➕ E0.6 plus
|
||||
2796 ; fully-qualified # ➖ E0.6 minus
|
||||
2797 ; fully-qualified # ➗ E0.6 divide
|
||||
1F7F0 ; fully-qualified # 🟰 E14.0 heavy equals sign
|
||||
267E FE0F ; fully-qualified # ♾️ E11.0 infinity
|
||||
267E ; unqualified # ♾ E11.0 infinity
|
||||
|
||||
|
@ -4581,8 +4693,8 @@
|
|||
1F533 ; fully-qualified # 🔳 E0.6 white square button
|
||||
1F532 ; fully-qualified # 🔲 E0.6 black square button
|
||||
|
||||
# Symbols subtotal: 301
|
||||
# Symbols subtotal: 301 w/o modifiers
|
||||
# Symbols subtotal: 302
|
||||
# Symbols subtotal: 302 w/o modifiers
|
||||
|
||||
# group: Flags
|
||||
|
||||
|
@ -4871,7 +4983,7 @@
|
|||
# Flags subtotal: 275 w/o modifiers
|
||||
|
||||
# Status Counts
|
||||
# fully-qualified : 3512
|
||||
# fully-qualified : 3624
|
||||
# minimally-qualified : 817
|
||||
# unqualified : 252
|
||||
# component : 9
|
||||
|
|
BIN
resources/icons/ui/refresh.png
Normal file
BIN
resources/icons/ui/refresh.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
16
resources/icons/ui/refresh.svg
Normal file
16
resources/icons/ui/refresh.svg
Normal 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
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
|
@ -3,7 +3,7 @@
|
|||
<component type="desktop">
|
||||
<id>nheko.desktop</id>
|
||||
<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>
|
||||
<summary>Desktop client for the Matrix protocol</summary>
|
||||
<description>
|
||||
|
|
|
@ -12,6 +12,7 @@ Rectangle {
|
|||
|
||||
property string url
|
||||
property string userid
|
||||
property string roomid
|
||||
property string displayName
|
||||
property alias textColor: label.color
|
||||
property bool crop: true
|
||||
|
@ -35,10 +36,29 @@ Rectangle {
|
|||
font.pixelSize: avatar.height / 2
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: img.status != Image.Ready
|
||||
visible: img.status != Image.Ready && !Settings.useIdenticon
|
||||
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 {
|
||||
id: img
|
||||
|
||||
|
@ -49,7 +69,7 @@ Rectangle {
|
|||
smooth: true
|
||||
sourceSize.width: avatar.width
|
||||
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 {
|
||||
id: mouseArea
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import "components"
|
||||
import im.nheko 1.0
|
||||
|
||||
// this needs to be last
|
||||
import QtQml 2.15
|
||||
|
||||
Rectangle {
|
||||
id: chatPage
|
||||
|
||||
|
@ -18,7 +21,7 @@ Rectangle {
|
|||
|
||||
anchors.fill: parent
|
||||
singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width
|
||||
pageIndex: Rooms.currentRoom ? 2 : 1
|
||||
pageIndex: (Rooms.currentRoom || Rooms.currentRoomPreview.roomid) ? 2 : 1
|
||||
|
||||
AdaptiveLayoutElement {
|
||||
id: communityListC
|
||||
|
@ -41,6 +44,7 @@ Rectangle {
|
|||
value: communityListC.preferredWidth
|
||||
when: !adaptiveView.singlePageMode
|
||||
delayed: true
|
||||
restoreMode: Binding.RestoreBindingOrValue
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -66,6 +70,7 @@ Rectangle {
|
|||
value: roomListC.preferredWidth
|
||||
when: !adaptiveView.singlePageMode
|
||||
delayed: true
|
||||
restoreMode: Binding.RestoreBindingOrValue
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,29 +47,32 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: ItemDelegate {
|
||||
id: communityItem
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
property color backgroundColor: Nheko.colors.window
|
||||
property color importantText: Nheko.colors.text
|
||||
property color unimportantText: Nheko.colors.buttonText
|
||||
property color bubbleBackground: Nheko.colors.highlight
|
||||
property color bubbleText: Nheko.colors.highlightedText
|
||||
|
||||
color: background
|
||||
background: Rectangle {
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
width: ListView.view.width
|
||||
state: "normal"
|
||||
ToolTip.visible: hovered.hovered && collapsed
|
||||
ToolTip.visible: hovered && collapsed
|
||||
ToolTip.text: model.tooltip
|
||||
states: [
|
||||
State {
|
||||
name: "highlight"
|
||||
when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id)
|
||||
when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId == model.id)
|
||||
|
||||
PropertyChanges {
|
||||
target: communityItem
|
||||
background: Nheko.colors.dark
|
||||
backgroundColor: Nheko.colors.dark
|
||||
importantText: Nheko.colors.brightText
|
||||
unimportantText: Nheko.colors.brightText
|
||||
bubbleBackground: Nheko.colors.highlight
|
||||
|
@ -83,7 +86,7 @@ Page {
|
|||
|
||||
PropertyChanges {
|
||||
target: communityItem
|
||||
background: Nheko.colors.highlight
|
||||
backgroundColor: Nheko.colors.highlight
|
||||
importantText: Nheko.colors.highlightedText
|
||||
unimportantText: Nheko.colors.highlightedText
|
||||
bubbleBackground: Nheko.colors.highlightedText
|
||||
|
@ -93,24 +96,20 @@ Page {
|
|||
}
|
||||
]
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
TapHandler {
|
||||
margin: -Nheko.paddingSmall
|
||||
acceptedButtons: Qt.RightButton
|
||||
onSingleTapped: communityContextMenu.show(model.id)
|
||||
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 {
|
||||
id: hovered
|
||||
|
||||
margin: -Nheko.paddingSmall
|
||||
}
|
||||
onClicked: Communities.setCurrentTagId(model.id)
|
||||
onPressAndHold: communityContextMenu.show(model.id)
|
||||
|
||||
RowLayout {
|
||||
spacing: Nheko.paddingMedium
|
||||
|
@ -130,8 +129,9 @@ Page {
|
|||
else
|
||||
return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
|
||||
}
|
||||
roomid: model.id
|
||||
displayName: model.displayName
|
||||
color: communityItem.background
|
||||
color: communityItem.backgroundColor
|
||||
}
|
||||
|
||||
ElidedLabel {
|
||||
|
|
|
@ -139,6 +139,7 @@ Popup {
|
|||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.displayName
|
||||
userid: model.userid
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
||||
}
|
||||
|
@ -194,6 +195,7 @@ Popup {
|
|||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.roomName
|
||||
roomid: model.roomid
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: {
|
||||
popup.completionClicked(completer.completionAt(model.index));
|
||||
|
@ -225,6 +227,7 @@ Popup {
|
|||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.roomName
|
||||
roomid: model.roomid
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ Image {
|
|||
case Crypto.TOFU:
|
||||
return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
|
||||
default:
|
||||
return qsTr("Encrypted by an unverified device");
|
||||
return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -80,14 +80,14 @@ Popup {
|
|||
completerPopup.completer.searchString = text;
|
||||
}
|
||||
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;
|
||||
completerPopup.up();
|
||||
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
completerPopup.down();
|
||||
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
|
||||
} else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
|
||||
completerPopup.up();
|
||||
else
|
||||
completerPopup.down();
|
||||
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||
completerPopup.finishCompletion();
|
||||
|
|
|
@ -93,7 +93,7 @@ Rectangle {
|
|||
TextArea {
|
||||
id: messageInput
|
||||
|
||||
property int completerTriggeredAt: -1
|
||||
property int completerTriggeredAt: 0
|
||||
|
||||
function insertCompletion(completion) {
|
||||
messageInput.remove(completerTriggeredAt, cursorPosition);
|
||||
|
@ -134,10 +134,9 @@ Rectangle {
|
|||
return ;
|
||||
|
||||
room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
|
||||
if (cursorPosition <= completerTriggeredAt) {
|
||||
completerTriggeredAt = -1;
|
||||
if (popup.opened && cursorPosition <= completerTriggeredAt)
|
||||
popup.close();
|
||||
}
|
||||
|
||||
if (popup.opened)
|
||||
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
|
||||
|
||||
|
@ -145,7 +144,7 @@ Rectangle {
|
|||
onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
||||
onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
|
||||
// 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: {
|
||||
if (event.matches(StandardKey.Paste)) {
|
||||
room.input.paste(false);
|
||||
|
@ -165,18 +164,20 @@ Rectangle {
|
|||
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
|
||||
messageInput.text = room.input.nextText();
|
||||
} else if (event.key == Qt.Key_At) {
|
||||
messageInput.openCompleter(cursorPosition, "user");
|
||||
messageInput.openCompleter(selectionStart, "user");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_Colon) {
|
||||
messageInput.openCompleter(cursorPosition, "emoji");
|
||||
messageInput.openCompleter(selectionStart, "emoji");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_NumberSign) {
|
||||
messageInput.openCompleter(cursorPosition, "roomAliases");
|
||||
messageInput.openCompleter(selectionStart, "roomAliases");
|
||||
popup.open();
|
||||
} else if (event.key == Qt.Key_Escape && popup.opened) {
|
||||
completerTriggeredAt = -1;
|
||||
popup.completerName = "";
|
||||
popup.close();
|
||||
event.accepted = true;
|
||||
} else if (event.matches(StandardKey.SelectAll) && popup.opened) {
|
||||
popup.completerName = "";
|
||||
popup.close();
|
||||
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||
if (popup.opened) {
|
||||
|
@ -194,6 +195,9 @@ Rectangle {
|
|||
} else if (event.key == Qt.Key_Tab) {
|
||||
event.accepted = true;
|
||||
if (popup.opened) {
|
||||
if (event.modifiers & Qt.ShiftModifier)
|
||||
popup.down();
|
||||
else
|
||||
popup.up();
|
||||
} else {
|
||||
var pos = cursorPosition - 1;
|
||||
|
@ -218,7 +222,7 @@ Rectangle {
|
|||
} else if (event.key == Qt.Key_Up && popup.opened) {
|
||||
event.accepted = true;
|
||||
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;
|
||||
popup.down();
|
||||
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
|
||||
|
@ -264,9 +268,8 @@ Rectangle {
|
|||
function onRoomChanged() {
|
||||
messageInput.clear();
|
||||
if (room)
|
||||
messageInput.append(room.input.text());
|
||||
messageInput.append(room.input.text);
|
||||
|
||||
messageInput.completerTriggeredAt = -1;
|
||||
popup.completerName = "";
|
||||
messageInput.forceActiveFocus();
|
||||
}
|
||||
|
@ -285,8 +288,8 @@ Rectangle {
|
|||
Completer {
|
||||
id: popup
|
||||
|
||||
x: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).x : 0
|
||||
y: messageInput.completerTriggeredAt >= 0 ? messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height : 0
|
||||
x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
|
||||
y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
|
|
@ -23,6 +23,8 @@ ScrollView {
|
|||
|
||||
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
|
||||
// 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()
|
||||
|
@ -33,7 +35,7 @@ ScrollView {
|
|||
verticalLayoutDirection: ListView.BottomToTop
|
||||
onCountChanged: {
|
||||
// Mark timeline as read
|
||||
if (atYEnd)
|
||||
if (atYEnd && room)
|
||||
model.currentIndex = 0;
|
||||
|
||||
}
|
||||
|
@ -233,8 +235,8 @@ ScrollView {
|
|||
id: dateBubble
|
||||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
visible: previousMessageDay !== day
|
||||
text: chat.model.formatDateSeparator(timestamp)
|
||||
visible: room && previousMessageDay !== day
|
||||
text: room ? room.formatDateSeparator(timestamp) : ""
|
||||
color: Nheko.colors.text
|
||||
height: Math.round(fontMetrics.height * 1.4)
|
||||
width: contentWidth * 1.2
|
||||
|
@ -257,10 +259,10 @@ ScrollView {
|
|||
|
||||
width: 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
|
||||
userid: userId
|
||||
onClicked: chat.model.openUserProfile(userId)
|
||||
onClicked: room.openUserProfile(userId)
|
||||
ToolTip.visible: avatarHover.hovered
|
||||
ToolTip.text: userid
|
||||
|
||||
|
@ -276,7 +278,7 @@ ScrollView {
|
|||
}
|
||||
|
||||
function onScrollToIndex(index) {
|
||||
chat.positionViewAtIndex(index, ListView.Visible);
|
||||
chat.positionViewAtIndex(index, ListView.Center);
|
||||
}
|
||||
|
||||
target: chat.model
|
||||
|
@ -361,7 +363,7 @@ ScrollView {
|
|||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
width: chat.delegateMaxWidth
|
||||
height: section ? section.height + timelinerow.height : timelinerow.height
|
||||
height: Math.max(section.active ? section.height + timelinerow.height : timelinerow.height, 10)
|
||||
|
||||
Rectangle {
|
||||
id: scrollHighlight
|
||||
|
@ -420,6 +422,7 @@ ScrollView {
|
|||
property string userName: wrapper.userName
|
||||
property var timestamp: wrapper.timestamp
|
||||
|
||||
z: 4
|
||||
active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day
|
||||
//asynchronous: true
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
38
resources/qml/NotificationWarning.qml
Normal file
38
resources/qml/NotificationWarning.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -39,14 +39,14 @@ Popup {
|
|||
completerPopup.completer.searchString = text;
|
||||
}
|
||||
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;
|
||||
completerPopup.up();
|
||||
} else if (event.key == Qt.Key_Down && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
completerPopup.down();
|
||||
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
|
||||
} else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
|
||||
event.accepted = true;
|
||||
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
|
||||
completerPopup.up();
|
||||
else
|
||||
completerPopup.down();
|
||||
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
|
||||
completerPopup.finishCompletion();
|
||||
|
|
|
@ -16,6 +16,14 @@ Page {
|
|||
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
|
||||
property bool collapsed: false
|
||||
|
||||
Component {
|
||||
id: roomDirectoryComponent
|
||||
|
||||
RoomDirectory {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ListView {
|
||||
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 {
|
||||
text: qsTr("Leave room")
|
||||
onTriggered: leaveRoomDialog.open()
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {
|
||||
|
@ -116,10 +114,10 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: ItemDelegate {
|
||||
id: roomItem
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
property color backgroundColor: Nheko.colors.window
|
||||
property color importantText: Nheko.colors.text
|
||||
property color unimportantText: Nheko.colors.buttonText
|
||||
property color bubbleBackground: Nheko.colors.highlight
|
||||
|
@ -135,21 +133,34 @@ Page {
|
|||
required property int notificationCount
|
||||
required property bool hasLoudNotification
|
||||
required property bool hasUnreadMessages
|
||||
required property bool isDirect
|
||||
required property string directChatOtherUserId
|
||||
|
||||
color: background
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
width: ListView.view.width
|
||||
state: "normal"
|
||||
ToolTip.visible: hovered.hovered && collapsed
|
||||
ToolTip.visible: hovered && collapsed
|
||||
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: [
|
||||
State {
|
||||
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 {
|
||||
target: roomItem
|
||||
background: Nheko.colors.dark
|
||||
backgroundColor: Nheko.colors.dark
|
||||
importantText: Nheko.colors.brightText
|
||||
unimportantText: Nheko.colors.brightText
|
||||
bubbleBackground: Nheko.colors.highlight
|
||||
|
@ -163,7 +174,7 @@ Page {
|
|||
|
||||
PropertyChanges {
|
||||
target: roomItem
|
||||
background: Nheko.colors.highlight
|
||||
backgroundColor: Nheko.colors.highlight
|
||||
importantText: Nheko.colors.highlightedText
|
||||
unimportantText: Nheko.colors.highlightedText
|
||||
bubbleBackground: Nheko.colors.highlightedText
|
||||
|
@ -189,27 +200,6 @@ Page {
|
|||
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 {
|
||||
|
@ -229,6 +219,8 @@ Page {
|
|||
width: avatarSize
|
||||
url: avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: roomName
|
||||
userid: isDirect ? directChatOtherUserId : ""
|
||||
roomid: roomId
|
||||
|
||||
Rectangle {
|
||||
id: collapsedNotificationBubble
|
||||
|
@ -359,6 +351,10 @@ Page {
|
|||
visible: hasUnreadMessages
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -500,6 +496,91 @@ Page {
|
|||
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 {
|
||||
|
@ -563,6 +644,10 @@ Page {
|
|||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Room directory")
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
onClicked: {
|
||||
var win = roomDirectoryComponent.createObject(timelineRoot);
|
||||
win.show();
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
|
|
|
@ -111,6 +111,30 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: logoutDialog
|
||||
|
||||
LogoutDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: joinRoomDialog
|
||||
|
||||
JoinRoomDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: leaveRoomComponent
|
||||
|
||||
LeaveRoomDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: {
|
||||
|
@ -120,6 +144,11 @@ Page {
|
|||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+A"
|
||||
onActivated: Rooms.nextRoomWithActivity()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+Down"
|
||||
onActivated: Rooms.nextRoom()
|
||||
|
@ -130,6 +159,20 @@ Page {
|
|||
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 {
|
||||
function onNewDeviceVerificationRequest(flow) {
|
||||
var dialog = deviceVerificationDialog.createObject(timelineRoot, {
|
||||
|
@ -138,6 +181,10 @@ Page {
|
|||
dialog.show();
|
||||
}
|
||||
|
||||
target: VerificationManager
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onOpenProfile(profile) {
|
||||
var userProfile = userProfileComponent.createObject(timelineRoot, {
|
||||
"profile": profile
|
||||
|
@ -176,6 +223,13 @@ Page {
|
|||
dialog.show();
|
||||
}
|
||||
|
||||
function onOpenLeaveRoomDialog(roomid) {
|
||||
var dialog = leaveRoomComponent.createObject(timelineRoot, {
|
||||
"roomId": roomid
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
target: TimelineManager
|
||||
}
|
||||
|
||||
|
@ -190,6 +244,94 @@ Page {
|
|||
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 {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
|
305
resources/qml/SelfVerificationCheck.qml
Normal file
305
resources/qml/SelfVerificationCheck.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
|
@ -24,7 +24,7 @@ Item {
|
|||
property bool showBackButton: false
|
||||
|
||||
Label {
|
||||
visible: !room && !TimelineManager.isInitialSync && !roomPreview
|
||||
visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No room open")
|
||||
font.pointSize: 24
|
||||
|
@ -84,14 +84,9 @@ Item {
|
|||
target: timelineView
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: room || roomPreview
|
||||
Layout.fillWidth: true
|
||||
|
||||
sourceComponent: MessageView {
|
||||
MessageView {
|
||||
implicitHeight: msgView.height - typingIndicator.height
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
@ -128,6 +123,9 @@ Item {
|
|||
color: Nheko.theme.separator
|
||||
}
|
||||
|
||||
NotificationWarning {
|
||||
}
|
||||
|
||||
ReplyPopup {
|
||||
}
|
||||
|
||||
|
@ -139,6 +137,7 @@ Item {
|
|||
ColumnLayout {
|
||||
id: preview
|
||||
|
||||
property string roomId: room ? room.roomId : (roomPreview ? roomPreview.roomid : "")
|
||||
property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "")
|
||||
property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "")
|
||||
property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "")
|
||||
|
@ -155,6 +154,7 @@ Item {
|
|||
|
||||
Avatar {
|
||||
url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
roomid: parent.roomId
|
||||
displayName: parent.roomName
|
||||
height: 130
|
||||
width: 130
|
||||
|
@ -163,7 +163,7 @@ Item {
|
|||
}
|
||||
|
||||
MatrixText {
|
||||
text: parent.roomName
|
||||
text: parent.roomName == "" ? qsTr("No preview available") : parent.roomName
|
||||
font.pixelSize: 24
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
@ -240,7 +240,7 @@ Item {
|
|||
anchors.margins: Nheko.paddingMedium
|
||||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
visible: room != null && room.isSpace && showBackButton
|
||||
visible: (room == null || room.isSpace) && showBackButton
|
||||
enabled: visible
|
||||
image: ":/icons/icons/ui/angle-pointing-to-left.png"
|
||||
ToolTip.visible: hovered
|
||||
|
|
|
@ -13,10 +13,13 @@ Rectangle {
|
|||
|
||||
property bool showBackButton: false
|
||||
property string roomName: room ? room.roomName : qsTr("No room selected")
|
||||
property string roomId: room ? room.roomId : ""
|
||||
property string avatarUrl: room ? room.roomAvatarUrl : ""
|
||||
property string roomTopic: room ? room.roomTopic : ""
|
||||
property bool isEncrypted: room ? room.isEncrypted : false
|
||||
property int trustlevel: room ? room.trustlevel : Crypto.Unverified
|
||||
property bool isDirect: room ? room.isDirect : false
|
||||
property string directChatOtherUserId: room ? room.directChatOtherUserId : ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: topLayout.height + Nheko.paddingMedium * 2
|
||||
|
@ -65,10 +68,12 @@ Rectangle {
|
|||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
url: avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
roomid: roomId
|
||||
userid: isDirect ? directChatOtherUserId : ""
|
||||
displayName: roomName
|
||||
onClicked: {
|
||||
if (room)
|
||||
TimelineManager.openRoomSettings(room.roomId);
|
||||
TimelineManager.openRoomSettings(roomId);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +114,7 @@ Rectangle {
|
|||
case Crypto.Verified:
|
||||
return qsTr("This room contains only verified devices.");
|
||||
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:
|
||||
return qsTr("This room contains unverified devices!");
|
||||
}
|
||||
|
@ -135,7 +140,7 @@ Rectangle {
|
|||
Platform.MenuItem {
|
||||
visible: room ? room.permissions.canInvite() : false
|
||||
text: qsTr("Invite users")
|
||||
onTriggered: TimelineManager.openInviteUsers(room.roomId)
|
||||
onTriggered: TimelineManager.openInviteUsers(roomId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
|
@ -145,12 +150,12 @@ Rectangle {
|
|||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Leave room")
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId)
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onTriggered: TimelineManager.openRoomSettings(room.roomId)
|
||||
onTriggered: TimelineManager.openRoomSettings(roomId)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -23,6 +23,8 @@ Rectangle {
|
|||
required property int index
|
||||
required property int selectedIndex
|
||||
property bool crop: true
|
||||
property alias roomid: avatar.roomid
|
||||
property alias userid: avatar.userid
|
||||
|
||||
color: background
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
|
|
41
resources/qml/components/MainWindowDialog.qml
Normal file
41
resources/qml/components/MainWindowDialog.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
|
@ -3,11 +3,11 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
ColumnLayout {
|
||||
Column {
|
||||
id: r
|
||||
|
||||
required property int encryptionError
|
||||
|
|
|
@ -14,6 +14,7 @@ Item {
|
|||
required property string body
|
||||
required property string filename
|
||||
required property bool isReply
|
||||
required property string eventId
|
||||
property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth)
|
||||
property double tempHeight: tempWidth * proportionalHeight
|
||||
property double divisor: isReply ? 5 : 3
|
||||
|
@ -37,6 +38,7 @@ Item {
|
|||
Image {
|
||||
id: img
|
||||
|
||||
visible: !mxcimage.loaded
|
||||
anchors.fill: parent
|
||||
source: url.replace("mxc://", "image://MxcImage/")
|
||||
asynchronous: true
|
||||
|
@ -53,6 +55,18 @@ Item {
|
|||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MxcAnimatedImage {
|
||||
id: mxcimage
|
||||
|
||||
visible: loaded
|
||||
anchors.fill: parent
|
||||
roomm: room
|
||||
play: !Settings.animateImagesOnHover || mouseArea.hovered
|
||||
eventId: parent.eventId
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: mouseArea
|
||||
}
|
||||
|
@ -88,5 +102,3 @@ Item {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
|
@ -32,7 +34,7 @@ Item {
|
|||
required property int encryptionError
|
||||
required property int relatedEventCacheBuster
|
||||
|
||||
height: chooser.childrenRect.height
|
||||
height: Math.max(chooser.child.height, 20)
|
||||
|
||||
DelegateChooser {
|
||||
id: chooser
|
||||
|
@ -100,6 +102,7 @@ Item {
|
|||
body: d.body
|
||||
filename: d.filename
|
||||
isReply: d.isReply
|
||||
eventId: d.eventId
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -116,6 +119,7 @@ Item {
|
|||
body: d.body
|
||||
filename: d.filename
|
||||
isReply: d.isReply
|
||||
eventId: d.eventId
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -357,6 +361,9 @@ Item {
|
|||
DelegateChoice {
|
||||
roleValue: MtxEvent.Member
|
||||
|
||||
ColumnLayout {
|
||||
width: parent ? parent.width : undefined
|
||||
|
||||
NoticeMessage {
|
||||
body: formatted
|
||||
isOnlyEmoji: false
|
||||
|
@ -364,6 +371,15 @@ Item {
|
|||
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 {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "../"
|
||||
import QtMultimedia 5.6
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.1
|
||||
import QtMultimedia 5.15
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
|
@ -40,25 +40,15 @@ ColumnLayout {
|
|||
|
||||
id: content
|
||||
Layout.maximumWidth: parent? parent.width: undefined
|
||||
MediaPlayer {
|
||||
id: media
|
||||
MxcMedia {
|
||||
id: mxcmedia
|
||||
// TODO: Show error in overlay or so?
|
||||
onError: console.log(errorString)
|
||||
volume: volumeSlider.desiredVolume
|
||||
onError: console.log(error)
|
||||
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 {
|
||||
// Display over video controls
|
||||
z: videoOutput.z + 1
|
||||
visible: !mediaCachedObserver.mediaCached
|
||||
visible: !mxcmedia.loaded
|
||||
anchors.fill: parent
|
||||
color: Nheko.colors.window
|
||||
opacity: 0.5
|
||||
|
@ -103,8 +93,8 @@ ColumnLayout {
|
|||
id: cacheVideoArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: !mediaCachedObserver.mediaCached
|
||||
onClicked: room.cacheMedia(eventId)
|
||||
enabled: !mxcmedia.loaded
|
||||
onClicked: mxcmedia.eventId = eventId
|
||||
}
|
||||
}
|
||||
VideoOutput {
|
||||
|
@ -112,7 +102,9 @@ ColumnLayout {
|
|||
clip: true
|
||||
anchors.fill: parent
|
||||
fillMode: VideoOutput.PreserveAspectFit
|
||||
source: media
|
||||
source: mxcmedia
|
||||
flushMode: VideoOutput.FirstFrame
|
||||
|
||||
// TODO: once we can use Qt 5.12, use HoverHandler
|
||||
MouseArea {
|
||||
id: playerMouseArea
|
||||
|
@ -120,9 +112,9 @@ ColumnLayout {
|
|||
onClicked: {
|
||||
if (controlRect.shouldShowControls &&
|
||||
!controlRect.contains(mapToItem(controlRect, mouseX, mouseY))) {
|
||||
(media.playbackState == MediaPlayer.PlayingState) ?
|
||||
media.pause() :
|
||||
media.play()
|
||||
(mxcmedia.state == MediaPlayer.PlayingState) ?
|
||||
mxcmedia.pause() :
|
||||
mxcmedia.play()
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
|
@ -159,7 +151,7 @@ ColumnLayout {
|
|||
property color controlColor: (playbackStateArea.containsMouse) ?
|
||||
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/play-sign.png?"+controlColor
|
||||
MouseArea {
|
||||
|
@ -168,25 +160,25 @@ ColumnLayout {
|
|||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
(media.playbackState == MediaPlayer.PlayingState) ?
|
||||
media.pause() :
|
||||
media.play()
|
||||
(mxcmedia.state == MediaPlayer.PlayingState) ?
|
||||
mxcmedia.pause() :
|
||||
mxcmedia.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: (!mediaCachedObserver.mediaCached) ? "-/-" :
|
||||
durationToString(media.position) + "/" + durationToString(media.duration)
|
||||
text: (!mxcmedia.loaded) ? "-/-" :
|
||||
durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
|
||||
}
|
||||
|
||||
Slider {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 50
|
||||
height: controlRect.controlHeight
|
||||
value: media.position
|
||||
onMoved: media.seek(value)
|
||||
value: mxcmedia.position
|
||||
onMoved: mxcmedia.position = value
|
||||
from: 0
|
||||
to: media.duration
|
||||
to: mxcmedia.duration
|
||||
}
|
||||
// Volume slider activator
|
||||
Image {
|
||||
|
@ -195,7 +187,7 @@ ColumnLayout {
|
|||
|
||||
// TODO: add icons for different volume levels
|
||||
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-off-indicator.png?"+ controlColor
|
||||
Layout.rightMargin: 5
|
||||
|
@ -205,7 +197,7 @@ ColumnLayout {
|
|||
id: volumeImageArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: media.muted = !media.muted
|
||||
onClicked: mxcmedia.muted = !mxcmedia.muted
|
||||
onExited: volumeSliderHideTimer.start()
|
||||
onPositionChanged: volumeSliderHideTimer.start()
|
||||
// For hiding volume slider after a while
|
||||
|
@ -248,7 +240,7 @@ ColumnLayout {
|
|||
id: volumeSlider
|
||||
from: 0
|
||||
to: 1
|
||||
value: (media.muted) ? 0 :
|
||||
value: (mxcmedia.muted) ? 0 :
|
||||
QtMultimedia.convertVolume(desiredVolume,
|
||||
QtMultimedia.LinearVolumeScale,
|
||||
QtMultimedia.LogarithmicVolumeScale)
|
||||
|
@ -262,7 +254,7 @@ ColumnLayout {
|
|||
QtMultimedia.LinearVolumeScale)
|
||||
/* This would be better handled in 'media', but it has some issue with listening
|
||||
to this signal */
|
||||
onDesiredVolumeChanged: media.muted = !(desiredVolume > 0)
|
||||
onDesiredVolumeChanged: mxcmedia.muted = !(desiredVolume > 0)
|
||||
}
|
||||
// Used for resetting the timer on mouse moves on volumeSliderRect
|
||||
MouseArea {
|
||||
|
@ -288,7 +280,7 @@ ColumnLayout {
|
|||
}
|
||||
// This breaks separation of concerns but this same thing doesn't work when called from controlRect...
|
||||
property bool shouldShowControls: (containsMouse && controlHideTimer.running) ||
|
||||
(media.playbackState != MediaPlayer.PlayingState) ||
|
||||
(mxcmedia.state != MediaPlayer.PlayingState) ||
|
||||
controlRect.contains(mapToItem(controlRect, mouseX, mouseY))
|
||||
|
||||
// For hiding controls on stationary cursor
|
||||
|
@ -331,9 +323,9 @@ ColumnLayout {
|
|||
Nheko.colors.highlight : Nheko.colors.text
|
||||
|
||||
source: {
|
||||
if (!mediaCachedObserver.mediaCached)
|
||||
if (!mxcmedia.loaded)
|
||||
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/play-sign.png?"+controlColor
|
||||
}
|
||||
|
@ -343,29 +335,29 @@ ColumnLayout {
|
|||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (!mediaCachedObserver.mediaCached) {
|
||||
room.cacheMedia(eventId)
|
||||
if (!mxcmedia.loaded) {
|
||||
mxcmedia.eventId = eventId
|
||||
return
|
||||
}
|
||||
(media.playbackState == MediaPlayer.PlayingState) ?
|
||||
media.pause() :
|
||||
media.play()
|
||||
(mxcmedia.state == MediaPlayer.PlayingState) ?
|
||||
mxcmedia.pause() :
|
||||
mxcmedia.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: (!mediaCachedObserver.mediaCached) ? "-/-" :
|
||||
durationToString(media.position) + "/" + durationToString(media.duration)
|
||||
text: (!mxcmedia.loaded) ? "-/-" :
|
||||
durationToString(mxcmedia.position) + "/" + durationToString(mxcmedia.duration)
|
||||
}
|
||||
|
||||
Slider {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 50
|
||||
height: controlRect.controlHeight
|
||||
value: media.position
|
||||
onMoved: media.seek(value)
|
||||
value: mxcmedia.position
|
||||
onMoved: mxcmedia.seek(value)
|
||||
from: 0
|
||||
to: media.duration
|
||||
to: mxcmedia.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
|
@ -36,11 +37,6 @@ Item {
|
|||
width: parent.width
|
||||
height: replyContainer.height
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: chat.model.showEvent(eventId)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
@ -62,6 +58,19 @@ Item {
|
|||
anchors.leftMargin: 4
|
||||
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 {
|
||||
id: userName_
|
||||
|
||||
|
@ -99,6 +108,7 @@ Item {
|
|||
callType: r.callType
|
||||
relatedEventCacheBuster: r.relatedEventCacheBuster
|
||||
encryptionError: r.encryptionError
|
||||
// This is disabled so that left clicking the reply goes to its location
|
||||
enabled: false
|
||||
width: parent.width
|
||||
isReply: true
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick.Controls 2.3
|
||||
import im.nheko 1.0
|
||||
|
||||
MatrixText {
|
||||
|
@ -28,6 +29,7 @@ MatrixText {
|
|||
border-collapse: collapse;
|
||||
border: 1px solid " + Nheko.colors.text + ";
|
||||
}
|
||||
blockquote { margin-left: 1em; }
|
||||
</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>")
|
||||
width: parent ? parent.width : undefined
|
||||
|
@ -35,4 +37,11 @@ MatrixText {
|
|||
clip: isReply
|
||||
selectByMouse: !Settings.mobileMode && !isReply
|
||||
font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
|
||||
|
||||
CursorShape {
|
||||
enabled: isReply
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ ApplicationWindow {
|
|||
|
||||
property var flow
|
||||
|
||||
onClosing: TimelineManager.removeVerificationFlow(flow)
|
||||
onClosing: VerificationManager.removeVerificationFlow(flow)
|
||||
title: stack.currentItem.title
|
||||
modality: Qt.NonModal
|
||||
palette: Nheko.colors
|
||||
height: stack.implicitHeight
|
||||
width: stack.implicitWidth
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(dialog)
|
||||
|
||||
StackView {
|
||||
|
|
|
@ -33,9 +33,9 @@ Pane {
|
|||
case DeviceVerificationFlow.User:
|
||||
return qsTr("Other party canceled the verification.");
|
||||
case DeviceVerificationFlow.OutOfOrder:
|
||||
return qsTr("Device verification timed out.");
|
||||
return qsTr("Verification messages received out of order!");
|
||||
default:
|
||||
return "Unknown verification error.";
|
||||
return qsTr("Unknown verification error.");
|
||||
}
|
||||
}
|
||||
color: Nheko.colors.text
|
||||
|
|
|
@ -23,6 +23,9 @@ Pane {
|
|||
text: {
|
||||
if (flow.sender) {
|
||||
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);
|
||||
else
|
||||
return qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party.");
|
||||
|
|
|
@ -27,7 +27,7 @@ ApplicationWindow {
|
|||
palette: Nheko.colors
|
||||
color: Nheko.colors.base
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
|
||||
AdaptiveLayout {
|
||||
id: adaptiveView
|
||||
|
@ -61,6 +61,7 @@ ApplicationWindow {
|
|||
header: AvatarListTile {
|
||||
title: imagePack.packname
|
||||
avatarUrl: imagePack.avatarUrl
|
||||
roomid: imagePack.statekey
|
||||
subtitle: imagePack.statekey
|
||||
index: -1
|
||||
selectedIndex: currentImageIndex
|
||||
|
@ -90,7 +91,7 @@ ApplicationWindow {
|
|||
|
||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
fileMode: FileDialog.OpenFiles
|
||||
nameFilters: [qsTr("Stickers (*.png *.webp)")]
|
||||
nameFilters: [qsTr("Stickers (*.png *.webp *.gif *.jpg *.jpeg)")]
|
||||
onAccepted: imagePack.addStickers(files)
|
||||
}
|
||||
|
||||
|
@ -142,6 +143,7 @@ ApplicationWindow {
|
|||
Layout.columnSpan: 2
|
||||
url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: imagePack.packname
|
||||
roomid: imagePack.statekey
|
||||
height: 130
|
||||
width: 130
|
||||
crop: false
|
||||
|
@ -219,6 +221,7 @@ ApplicationWindow {
|
|||
Layout.columnSpan: 2
|
||||
url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/")
|
||||
displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
|
||||
roomid: displayName
|
||||
height: 130
|
||||
width: 130
|
||||
crop: false
|
||||
|
@ -265,6 +268,20 @@ ApplicationWindow {
|
|||
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 {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillHeight: true
|
||||
|
|
|
@ -25,7 +25,7 @@ ApplicationWindow {
|
|||
palette: Nheko.colors
|
||||
color: Nheko.colors.base
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(win)
|
||||
|
||||
Component {
|
||||
|
@ -101,6 +101,7 @@ ApplicationWindow {
|
|||
required property string displayName
|
||||
required property bool fromAccountData
|
||||
required property bool fromCurrentRoom
|
||||
required property string statekey
|
||||
|
||||
title: displayName
|
||||
subtitle: {
|
||||
|
@ -112,6 +113,7 @@ ApplicationWindow {
|
|||
return qsTr("Globally enabled pack");
|
||||
}
|
||||
selectedIndex: currentPackIndex
|
||||
roomid: statekey
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: currentPackIndex = index
|
||||
|
@ -135,6 +137,7 @@ ApplicationWindow {
|
|||
property string packName: currentPack ? currentPack.packname : ""
|
||||
property string attribution: currentPack ? currentPack.attribution : ""
|
||||
property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
|
||||
property string statekey: currentPack ? currentPack.statekey : ""
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingLarge
|
||||
|
@ -143,6 +146,7 @@ ApplicationWindow {
|
|||
Avatar {
|
||||
url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: packinfo.packName
|
||||
roomid: packinfo.statekey
|
||||
height: 100
|
||||
width: 100
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
|
|
@ -12,6 +12,7 @@ ApplicationWindow {
|
|||
id: inputDialog
|
||||
|
||||
property alias prompt: promptLabel.text
|
||||
property alias echoMode: statusInput.echoMode
|
||||
property var onAccepted: undefined
|
||||
|
||||
modality: Qt.NonModal
|
||||
|
@ -21,7 +22,8 @@ ApplicationWindow {
|
|||
height: fontMetrics.lineSpacing * 7
|
||||
|
||||
ColumnLayout {
|
||||
anchors.margins: Nheko.paddingLarge
|
||||
spacing: Nheko.paddingMedium
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
anchors.fill: parent
|
||||
|
||||
Label {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
@ -34,7 +35,7 @@ ApplicationWindow {
|
|||
width: 340
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(inviteDialogRoot)
|
||||
|
||||
Shortcut {
|
78
resources/qml/dialogs/JoinRoomDialog.qml
Normal file
78
resources/qml/dialogs/JoinRoomDialog.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
20
resources/qml/dialogs/LeaveRoomDialog.qml
Normal file
20
resources/qml/dialogs/LeaveRoomDialog.qml
Normal 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)
|
||||
}
|
19
resources/qml/dialogs/LogoutDialog.qml
Normal file
19
resources/qml/dialogs/LogoutDialog.qml
Normal 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()
|
||||
}
|
1744
resources/qml/dialogs/PhoneNumberInputDialog.qml
Normal file
1744
resources/qml/dialogs/PhoneNumberInputDialog.qml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@ ApplicationWindow {
|
|||
width: 420
|
||||
palette: Nheko.colors
|
||||
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)
|
||||
|
||||
Shortcut {
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
@ -19,7 +20,7 @@ ApplicationWindow {
|
|||
minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(readReceiptsRoot)
|
||||
|
||||
Shortcut {
|
217
resources/qml/dialogs/RoomDirectory.qml
Normal file
217
resources/qml/dialogs/RoomDirectory.qml
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./ui"
|
||||
import ".."
|
||||
import "../ui"
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
@ -21,7 +22,7 @@ ApplicationWindow {
|
|||
minimumHeight: 420
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(roomMembersRoot)
|
||||
|
||||
Shortcut {
|
||||
|
@ -39,6 +40,7 @@ ApplicationWindow {
|
|||
|
||||
width: 130
|
||||
height: width
|
||||
roomid: members.roomId
|
||||
displayName: members.roomName
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
url: members.avatarUrl.replace("mxc://", "image://MxcImage/")
|
|
@ -2,7 +2,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./ui"
|
||||
import ".."
|
||||
import "../ui"
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.3
|
||||
|
@ -20,7 +21,7 @@ ApplicationWindow {
|
|||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(roomSettingsDialog)
|
||||
title: qsTr("Room Settings")
|
||||
|
||||
|
@ -38,6 +39,7 @@ ApplicationWindow {
|
|||
|
||||
Avatar {
|
||||
url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
roomid: roomSettings.roomId
|
||||
displayName: roomSettings.roomName
|
||||
height: 130
|
||||
width: 130
|
||||
|
@ -186,7 +188,16 @@ ApplicationWindow {
|
|||
|
||||
ComboBox {
|
||||
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
|
||||
onActivated: {
|
||||
roomSettings.changeAccessRules(index);
|
433
resources/qml/dialogs/UserProfile.qml
Normal file
433
resources/qml/dialogs/UserProfile.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -72,7 +72,8 @@ Menu {
|
|||
onVisibleChanged: {
|
||||
if (visible)
|
||||
forceActiveFocus();
|
||||
|
||||
else
|
||||
clear();
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
|
|
@ -34,14 +34,15 @@ Rectangle {
|
|||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
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)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.leftMargin: 8
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
text: CallManager.callParty
|
||||
text: CallManager.callPartyDisplayName
|
||||
color: "#000000"
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ Popup {
|
|||
Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: msgView.height / 25
|
||||
text: CallManager.callParty
|
||||
text: CallManager.callPartyDisplayName
|
||||
font.pointSize: fontMetrics.font.pointSize * 2
|
||||
color: Nheko.colors.windowText
|
||||
}
|
||||
|
@ -50,7 +50,8 @@ Popup {
|
|||
width: msgView.height / 5
|
||||
height: msgView.height / 5
|
||||
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: CallManager.callParty
|
||||
userid: CallManager.callParty
|
||||
displayName: CallManager.callPartyDisplayName
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
|
|
@ -41,14 +41,15 @@ Rectangle {
|
|||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
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)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.leftMargin: 8
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
text: CallManager.callParty
|
||||
text: CallManager.callPartyDisplayName
|
||||
color: "#000000"
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ Popup {
|
|||
height: Nheko.avatarSize
|
||||
url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: room.roomName
|
||||
roomid: room.roomid
|
||||
onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
|
||||
}
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
<file>icons/ui/screen-share.png</file>
|
||||
<file>icons/ui/toggle-camera-view.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@2x.png</file>
|
||||
<file>icons/emoji-categories/nature.png</file>
|
||||
|
@ -138,31 +139,47 @@
|
|||
<file>qml/TopBar.qml</file>
|
||||
<file>qml/QuickSwitcher.qml</file>
|
||||
<file>qml/ForwardCompleter.qml</file>
|
||||
<file>qml/SelfVerificationCheck.qml</file>
|
||||
<file>qml/TypingIndicator.qml</file>
|
||||
<file>qml/RoomSettings.qml</file>
|
||||
<file>qml/emoji/EmojiPicker.qml</file>
|
||||
<file>qml/emoji/StickerPicker.qml</file>
|
||||
<file>qml/UserProfile.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/NotificationWarning.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/components/MainWindowDialog.qml</file>
|
||||
<file>qml/delegates/Encrypted.qml</file>
|
||||
<file>qml/delegates/FileMessage.qml</file>
|
||||
<file>qml/delegates/ImageMessage.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/delegates/NoticeMessage.qml</file>
|
||||
<file>qml/delegates/Pill.qml</file>
|
||||
<file>qml/delegates/Placeholder.qml</file>
|
||||
<file>qml/delegates/PlayableMediaMessage.qml</file>
|
||||
<file>qml/delegates/Reply.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/DigitVerification.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/NewVerificationRequest.qml</file>
|
||||
<file>qml/device-verification/Success.qml</file>
|
||||
<file>qml/dialogs/InputDialog.qml</file>
|
||||
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
||||
<file>qml/device-verification/Waiting.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/Spinner.qml</file>
|
||||
<file>qml/ui/animations/BlinkAnimation.qml</file>
|
||||
|
@ -174,14 +191,6 @@
|
|||
<file>qml/voip/PlaceCall.qml</file>
|
||||
<file>qml/voip/ScreenShare.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 prefix="/media">
|
||||
<file>media/ring.ogg</file>
|
||||
|
|
|
@ -54,7 +54,7 @@ if __name__ == '__main__':
|
|||
}
|
||||
|
||||
current_category = ''
|
||||
for line in open(filename, 'r'):
|
||||
for line in open(filename, 'r', encoding="utf8"):
|
||||
if line.startswith('# group:'):
|
||||
current_category = line.split(':', 1)[1].strip()
|
||||
|
||||
|
|
|
@ -48,8 +48,7 @@ resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback)
|
|||
recv,
|
||||
[callback, cacheKey](QPixmap pm) {
|
||||
if (!pm.isNull())
|
||||
avatar_cache.insert(
|
||||
cacheKey, pm);
|
||||
avatar_cache.insert(cacheKey, pm);
|
||||
callback(pm);
|
||||
});
|
||||
|
||||
|
|
706
src/Cache.cpp
706
src/Cache.cpp
File diff suppressed because it is too large
Load diff
|
@ -83,6 +83,9 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
|
|||
//! Retrieve member info from a room.
|
||||
std::vector<RoomMember>
|
||||
getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30);
|
||||
//! 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
|
||||
isInitialized();
|
||||
|
|
|
@ -47,6 +47,11 @@ struct GroupSessionData
|
|||
uint64_t message_index = 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::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.
|
||||
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.
|
||||
std::string room_id;
|
||||
//! The session_id of the megolm session.
|
||||
|
@ -167,3 +179,16 @@ void
|
|||
to_json(nlohmann::json &j, const VerificationCache &info);
|
||||
void
|
||||
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);
|
||||
|
|
|
@ -93,7 +93,7 @@ to_json(nlohmann::json &j, const RoomInfo &info);
|
|||
void
|
||||
from_json(const nlohmann::json &j, RoomInfo &info);
|
||||
|
||||
//! Basic information per member;
|
||||
//! Basic information per member.
|
||||
struct MemberInfo
|
||||
{
|
||||
std::string name;
|
||||
|
|
119
src/Cache_p.h
119
src/Cache_p.h
|
@ -49,15 +49,12 @@ public:
|
|||
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
|
||||
const std::string &room_id,
|
||||
bool verified_only);
|
||||
void updateUserKeys(const std::string &sync_token,
|
||||
const mtx::responses::QueryKeys &keyQuery);
|
||||
void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
|
||||
void markUserKeysOutOfDate(const std::vector<std::string> &user_ids);
|
||||
void markUserKeysOutOfDate(lmdb::txn &txn,
|
||||
lmdb::dbi &db,
|
||||
const std::vector<std::string> &user_ids,
|
||||
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,
|
||||
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
|
||||
|
||||
|
@ -109,6 +106,10 @@ public:
|
|||
std::vector<RoomMember> getMembers(const std::string &room_id,
|
||||
std::size_t startIndex = 0,
|
||||
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);
|
||||
|
||||
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.
|
||||
//! The user id should be removed from any other lists.
|
||||
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
|
||||
void updateReadReceipt(lmdb::txn &txn,
|
||||
const std::string &room_id,
|
||||
const Receipts &receipts);
|
||||
void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts);
|
||||
|
||||
//! Retrieve all the read receipts for the given event id and room.
|
||||
//!
|
||||
|
@ -186,8 +185,7 @@ public:
|
|||
uint64_t index = std::numeric_limits<uint64_t>::max(),
|
||||
bool forward = false);
|
||||
|
||||
std::optional<mtx::events::collections::TimelineEvent> getEvent(
|
||||
const std::string &room_id,
|
||||
std::optional<mtx::events::collections::TimelineEvent> getEvent(const std::string &room_id,
|
||||
const std::string &event_id);
|
||||
void storeEvent(const std::string &room_id,
|
||||
const std::string &event_id,
|
||||
|
@ -195,24 +193,20 @@ public:
|
|||
void replaceEvent(const std::string &room_id,
|
||||
const std::string &event_id,
|
||||
const mtx::events::collections::TimelineEvent &event);
|
||||
std::vector<std::string> relatedEvents(const std::string &room_id,
|
||||
const std::string &event_id);
|
||||
std::vector<std::string> relatedEvents(const std::string &room_id, const std::string &event_id);
|
||||
|
||||
struct TimelineRange
|
||||
{
|
||||
uint64_t first, last;
|
||||
};
|
||||
std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
|
||||
std::optional<uint64_t> getTimelineIndex(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<uint64_t> getTimelineIndex(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(
|
||||
const std::string &room_id,
|
||||
std::string_view event_id);
|
||||
std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
|
||||
std::optional<uint64_t> getArrivalIndex(const std::string &room_id,
|
||||
std::string_view event_id);
|
||||
std::optional<uint64_t> getArrivalIndex(const std::string &room_id, std::string_view event_id);
|
||||
|
||||
std::string previousBatchToken(const std::string &room_id);
|
||||
uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
|
||||
|
@ -267,8 +261,7 @@ public:
|
|||
void saveInboundMegolmSession(const MegolmSessionIndex &index,
|
||||
mtx::crypto::InboundGroupSessionPtr session,
|
||||
const GroupSessionData &data);
|
||||
mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(
|
||||
const MegolmSessionIndex &index);
|
||||
mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index);
|
||||
bool inboundMegolmSessionExists(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::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519,
|
||||
const std::string &session_id);
|
||||
std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(
|
||||
const std::string &curve25519);
|
||||
std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(const std::string &curve25519);
|
||||
|
||||
void saveOlmAccount(const std::string &pickled);
|
||||
std::string restoreOlmAccount();
|
||||
|
||||
void storeSecret(const std::string name, const std::string secret);
|
||||
void deleteSecret(const std::string name);
|
||||
std::optional<std::string> secret(const std::string name);
|
||||
void saveBackupVersion(const OnlineBackupVersion &data);
|
||||
void deleteBackupVersion();
|
||||
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>
|
||||
constexpr static bool isStateEvent_ =
|
||||
|
@ -300,21 +298,19 @@ public:
|
|||
{
|
||||
auto get_skey = [](const MDB_val *v) {
|
||||
return nlohmann::json::parse(
|
||||
std::string_view(static_cast<const char *>(v->mv_data),
|
||||
v->mv_size))
|
||||
std::string_view(static_cast<const char *>(v->mv_data), v->mv_size))
|
||||
.value("key", "");
|
||||
};
|
||||
|
||||
return get_skey(a).compare(get_skey(b));
|
||||
}
|
||||
|
||||
signals:
|
||||
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
|
||||
void roomReadStatus(const std::map<QString, bool> &status);
|
||||
void removeNotification(const QString &room_id, const QString &event_id);
|
||||
void userKeysUpdate(const std::string &sync_token,
|
||||
const mtx::responses::QueryKeys &keyQuery);
|
||||
void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
|
||||
void verificationStatusChanged(const std::string &userid);
|
||||
void selfVerificationStatusChanged();
|
||||
void secretChanged(const std::string name);
|
||||
|
||||
private:
|
||||
|
@ -388,9 +384,8 @@ private:
|
|||
//
|
||||
case Membership::Invite:
|
||||
case Membership::Join: {
|
||||
auto display_name = e->content.display_name.empty()
|
||||
? e->state_key
|
||||
: e->content.display_name;
|
||||
auto display_name =
|
||||
e->content.display_name.empty() ? e->state_key : e->content.display_name;
|
||||
|
||||
// Lightweight representation of a member.
|
||||
MemberInfo tmp{display_name, e->content.avatar_url};
|
||||
|
@ -416,17 +411,14 @@ private:
|
|||
eventsDb.put(txn, e.event_id, json(e).dump());
|
||||
|
||||
if (e.type != EventType::Unsupported) {
|
||||
if (std::is_same_v<
|
||||
std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(e)>>,
|
||||
if (std::is_same_v<std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
|
||||
StateEvent<mtx::events::msg::Redacted>>) {
|
||||
if (e.type == EventType::RoomMember)
|
||||
membersdb.del(txn, e.state_key, "");
|
||||
else if (e.state_key.empty())
|
||||
statesdb.del(txn, to_string(e.type));
|
||||
else
|
||||
stateskeydb.del(
|
||||
txn,
|
||||
stateskeydb.del(txn,
|
||||
to_string(e.type),
|
||||
json::object({
|
||||
{"key", e.state_key},
|
||||
|
@ -434,11 +426,9 @@ private:
|
|||
})
|
||||
.dump());
|
||||
} else if (e.state_key.empty())
|
||||
statesdb.put(
|
||||
txn, to_string(e.type), json(e).dump());
|
||||
statesdb.put(txn, to_string(e.type), json(e).dump());
|
||||
else
|
||||
stateskeydb.put(
|
||||
txn,
|
||||
stateskeydb.put(txn,
|
||||
to_string(e.type),
|
||||
json::object({
|
||||
{"key", e.state_key},
|
||||
|
@ -482,8 +472,7 @@ private:
|
|||
|
||||
try {
|
||||
auto eventsDb = getEventsDb(txn, room_id);
|
||||
if (!eventsDb.get(
|
||||
txn, json::parse(data)["id"].get<std::string>(), value))
|
||||
if (!eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value))
|
||||
return std::nullopt;
|
||||
} catch (std::exception &e) {
|
||||
return std::nullopt;
|
||||
|
@ -522,16 +511,11 @@ private:
|
|||
auto cursor = lmdb::cursor::open(txn, db);
|
||||
bool first = true;
|
||||
if (cursor.get(typeStrV, data, MDB_SET)) {
|
||||
while (cursor.get(
|
||||
typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
|
||||
while (cursor.get(typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
|
||||
first = false;
|
||||
|
||||
if (eventsDb.get(txn,
|
||||
json::parse(data)["id"].get<std::string>(),
|
||||
value))
|
||||
events.push_back(
|
||||
json::parse(value)
|
||||
.get<mtx::events::StateEvent<T>>());
|
||||
if (eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value))
|
||||
events.push_back(json::parse(value).get<mtx::events::StateEvent<T>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -580,14 +564,12 @@ private:
|
|||
// inverse of EventOrderDb
|
||||
lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
|
||||
{
|
||||
return lmdb::dbi::open(
|
||||
txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
|
||||
return lmdb::dbi::open(txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
|
||||
}
|
||||
|
||||
lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
|
||||
{
|
||||
return lmdb::dbi::open(
|
||||
txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
|
||||
return lmdb::dbi::open(txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return lmdb::dbi::open(
|
||||
txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
|
||||
return lmdb::dbi::open(txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
|
||||
}
|
||||
|
||||
lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
|
||||
{
|
||||
return lmdb::dbi::open(
|
||||
txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
|
||||
return lmdb::dbi::open(txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
|
||||
}
|
||||
|
||||
lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
|
||||
|
@ -635,8 +615,7 @@ private:
|
|||
|
||||
lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
|
||||
{
|
||||
return lmdb::dbi::open(
|
||||
txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
|
||||
return lmdb::dbi::open(txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
lmdb::dbi getPresenceDb(lmdb::txn &txn)
|
||||
{
|
||||
return lmdb::dbi::open(txn, "presence", MDB_CREATE);
|
||||
}
|
||||
lmdb::dbi getPresenceDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "presence", MDB_CREATE); }
|
||||
|
||||
lmdb::dbi getUserKeysDb(lmdb::txn &txn)
|
||||
{
|
||||
return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
|
||||
}
|
||||
lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); }
|
||||
|
||||
lmdb::dbi getVerificationDb(lmdb::txn &txn)
|
||||
{
|
||||
|
@ -682,13 +655,11 @@ private:
|
|||
return QString::fromStdString(event.state_key);
|
||||
}
|
||||
|
||||
std::optional<VerificationCache> verificationCache(const std::string &user_id,
|
||||
lmdb::txn &txn);
|
||||
std::optional<VerificationCache> verificationCache(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);
|
||||
|
||||
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
|
||||
void setNextBatchToken(lmdb::txn &txn, const QString &token);
|
||||
|
||||
lmdb::env env_;
|
||||
lmdb::dbi syncStateDb_;
|
||||
|
@ -710,6 +681,8 @@ private:
|
|||
QString localUserId_;
|
||||
QString cacheDirectory_;
|
||||
|
||||
std::string pickle_secret_;
|
||||
|
||||
VerificationStorage verification_storage;
|
||||
|
||||
bool databaseReady_ = false;
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
};
|
417
src/ChatPage.cpp
417
src/ChatPage.cpp
|
@ -6,26 +6,25 @@
|
|||
#include <QApplication>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
|
||||
#include <mtx/responses.hpp>
|
||||
|
||||
#include "AvatarProvider.h"
|
||||
#include "Cache.h"
|
||||
#include "Cache_p.h"
|
||||
#include "CallManager.h"
|
||||
#include "ChatPage.h"
|
||||
#include "DeviceVerificationFlow.h"
|
||||
#include "EventAccessors.h"
|
||||
#include "Logging.h"
|
||||
#include "MainWindow.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "Olm.h"
|
||||
#include "UserSettingsPage.h"
|
||||
#include "Utils.h"
|
||||
#include "encryption/DeviceVerificationFlow.h"
|
||||
#include "encryption/Olm.h"
|
||||
#include "ui/OverlayModal.h"
|
||||
#include "ui/Theme.h"
|
||||
#include "ui/UserProfile.h"
|
||||
#include "voip/CallManager.h"
|
||||
|
||||
#include "notifications/Manager.h"
|
||||
|
||||
|
@ -33,9 +32,6 @@
|
|||
|
||||
#include "blurhash.hpp"
|
||||
|
||||
// TODO: Needs to be updated with an actual secret.
|
||||
static const std::string STORAGE_SECRET_KEY("secret");
|
||||
|
||||
ChatPage *ChatPage::instance_ = nullptr;
|
||||
constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
|
||||
constexpr int RETRY_TIMEOUT = 5'000;
|
||||
|
@ -125,11 +121,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
http::client()->invite_user(
|
||||
roomId.toStdString(),
|
||||
user.toStdString(),
|
||||
[this, user](const mtx::responses::RoomInvite &,
|
||||
mtx::http::RequestErr err) {
|
||||
[this, user](const mtx::responses::RoomInvite &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
emit showNotification(
|
||||
tr("Failed to invite user: %1").arg(user));
|
||||
emit showNotification(tr("Failed to invite user: %1").arg(user));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -140,7 +134,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
});
|
||||
|
||||
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::highlightedNotifsRetrieved,
|
||||
|
@ -193,22 +187,33 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
|
||||
view_manager_->sync(rooms);
|
||||
|
||||
bool hasNotifications = false;
|
||||
static unsigned int prevNotificationCount = 0;
|
||||
unsigned int notificationCount = 0;
|
||||
for (const auto &room : rooms.join) {
|
||||
if (room.second.unread_notifications.notification_count > 0)
|
||||
hasNotifications = true;
|
||||
notificationCount += room.second.unread_notifications.notification_count;
|
||||
}
|
||||
|
||||
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(
|
||||
5,
|
||||
"",
|
||||
"",
|
||||
[this](const mtx::responses::Notifications &res,
|
||||
mtx::http::RequestErr err) {
|
||||
[this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn(
|
||||
"failed to retrieve notifications: {} ({})",
|
||||
nhlog::net()->warn("failed to retrieve notifications: {} ({})",
|
||||
err->matrix_error.error,
|
||||
static_cast<int>(err->status_code));
|
||||
return;
|
||||
|
@ -228,11 +233,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
[this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this,
|
||||
&ChatPage::newSyncResponse,
|
||||
this,
|
||||
&ChatPage::handleSyncResponse,
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
this, &ChatPage::newSyncResponse, this, &ChatPage::handleSyncResponse, Qt::QueuedConnection);
|
||||
|
||||
connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
|
||||
|
||||
|
@ -277,15 +279,15 @@ ChatPage::resetUI()
|
|||
void
|
||||
ChatPage::deleteConfigs()
|
||||
{
|
||||
QSettings settings;
|
||||
auto settings = UserSettings::instance()->qsettings();
|
||||
|
||||
if (UserSettings::instance()->profile() != "") {
|
||||
settings.beginGroup("profile");
|
||||
settings.beginGroup(UserSettings::instance()->profile());
|
||||
settings->beginGroup("profile");
|
||||
settings->beginGroup(UserSettings::instance()->profile());
|
||||
}
|
||||
settings.beginGroup("auth");
|
||||
settings.remove("");
|
||||
settings.endGroup(); // auth
|
||||
settings->beginGroup("auth");
|
||||
settings->remove("");
|
||||
settings->endGroup(); // auth
|
||||
|
||||
http::client()->shutdown();
|
||||
cache::deleteData();
|
||||
|
@ -299,14 +301,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
|
|||
try {
|
||||
http::client()->set_user(parse<User>(userid.toStdString()));
|
||||
} catch (const std::invalid_argument &) {
|
||||
nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
|
||||
userid.toStdString());
|
||||
nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString());
|
||||
}
|
||||
|
||||
http::client()->set_server(homeserver.toStdString());
|
||||
http::client()->set_access_token(token.toStdString());
|
||||
http::client()->verify_certificates(
|
||||
!UserSettings::instance()->disableCertificateValidation());
|
||||
http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
|
||||
|
||||
// The Olm client needs the user_id & device_id that will be included
|
||||
// in the generated payloads & keys.
|
||||
|
@ -339,8 +339,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
|
|||
return;
|
||||
} else if (cacheVersion == cache::CacheVersion::Older) {
|
||||
if (!cache::runMigrations()) {
|
||||
QMessageBox::critical(
|
||||
this,
|
||||
QMessageBox::critical(this,
|
||||
tr("Cache migration failed!"),
|
||||
tr("Migrating the cache to the current version failed. "
|
||||
"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.
|
||||
nhlog::crypto()->info("creating new olm 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) {
|
||||
nhlog::crypto()->critical("failed to save olm account {}", e.what());
|
||||
emit dropToLoginPageCb(QString::fromStdString(e.what()));
|
||||
|
@ -385,6 +384,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
|
|||
}
|
||||
|
||||
getProfileInfo();
|
||||
getBackupVersion();
|
||||
tryInitialSync();
|
||||
}
|
||||
|
||||
|
@ -394,7 +394,7 @@ ChatPage::loadStateFromCache()
|
|||
nhlog::db()->info("restoring state from cache");
|
||||
|
||||
try {
|
||||
olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
|
||||
olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret());
|
||||
|
||||
emit initializeEmptyViews();
|
||||
emit initializeMentions(cache::getTimelineMentions());
|
||||
|
@ -411,6 +411,11 @@ ChatPage::loadStateFromCache()
|
|||
return;
|
||||
} catch (const json::exception &e) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -418,6 +423,8 @@ ChatPage::loadStateFromCache()
|
|||
nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
|
||||
|
||||
getProfileInfo();
|
||||
getBackupVersion();
|
||||
verifyOneTimeKeyCountAfterStartup();
|
||||
|
||||
emit contentLoaded();
|
||||
|
||||
|
@ -459,15 +466,10 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
|
|||
if (isRoomActive(room_id))
|
||||
continue;
|
||||
|
||||
if (userSettings_->hasAlertOnNotification()) {
|
||||
QApplication::alert(this);
|
||||
}
|
||||
|
||||
if (userSettings_->hasDesktopNotifications()) {
|
||||
auto info = cache::singleRoomInfo(item.room_id);
|
||||
|
||||
AvatarProvider::resolve(
|
||||
QString::fromStdString(info.avatar_url),
|
||||
AvatarProvider::resolve(QString::fromStdString(info.avatar_url),
|
||||
96,
|
||||
this,
|
||||
[this, item](QPixmap image) {
|
||||
|
@ -499,14 +501,12 @@ ChatPage::tryInitialSync()
|
|||
const int status_code = static_cast<int>(err->status_code);
|
||||
|
||||
if (status_code == 404) {
|
||||
nhlog::net()->warn(
|
||||
"skipping key uploading. server doesn't provide /keys/upload");
|
||||
nhlog::net()->warn("skipping key uploading. server doesn't provide /keys/upload");
|
||||
return startInitialSync();
|
||||
}
|
||||
|
||||
nhlog::crypto()->critical("failed to upload one time keys: {} {}",
|
||||
err->matrix_error.error,
|
||||
status_code);
|
||||
nhlog::crypto()->critical(
|
||||
"failed to upload one time keys: {} {}", err->matrix_error.error, status_code);
|
||||
|
||||
QString errorMsg(tr("Failed to setup encryption keys. Server response: "
|
||||
"%1 %2. Please try again later.")
|
||||
|
@ -520,8 +520,7 @@ ChatPage::tryInitialSync()
|
|||
olm::mark_keys_as_published();
|
||||
|
||||
for (const auto &entry : res.one_time_key_counts)
|
||||
nhlog::net()->info(
|
||||
"uploaded {} {} one-time keys", entry.second, entry.first);
|
||||
nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first);
|
||||
|
||||
startInitialSync();
|
||||
});
|
||||
|
@ -536,8 +535,7 @@ ChatPage::startInitialSync()
|
|||
opts.timeout = 0;
|
||||
opts.set_presence = currentPresence();
|
||||
|
||||
http::client()->sync(
|
||||
opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
|
||||
http::client()->sync(opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
|
||||
// TODO: Initial Sync should include mentions as well...
|
||||
|
||||
if (err) {
|
||||
|
@ -584,8 +582,7 @@ ChatPage::startInitialSync()
|
|||
|
||||
cache::calculateRoomReadStatus();
|
||||
} catch (const lmdb::error &e) {
|
||||
nhlog::db()->error("failed to save state after initial sync: {}",
|
||||
e.what());
|
||||
nhlog::db()->error("failed to save state after initial sync: {}", e.what());
|
||||
startInitialSync();
|
||||
return;
|
||||
}
|
||||
|
@ -655,8 +652,7 @@ ChatPage::trySync()
|
|||
}
|
||||
|
||||
http::client()->sync(
|
||||
opts,
|
||||
[this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
|
||||
opts, [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
const auto error = QString::fromStdString(err->matrix_error.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);
|
||||
|
||||
if ((http::is_logged_in() &&
|
||||
(err->matrix_error.errcode ==
|
||||
mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
|
||||
err->matrix_error.errcode ==
|
||||
mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
|
||||
(err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
|
||||
err->matrix_error.errcode == mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
|
||||
!http::is_logged_in()) {
|
||||
emit dropToLoginPageCb(msg);
|
||||
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) {
|
||||
if (err) {
|
||||
emit showNotification(
|
||||
tr("Failed to join room: %1")
|
||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||
tr("Failed to join room: %1").arg(QString::fromStdString(err->matrix_error.error)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -738,8 +731,7 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
|
|||
const auto error = err->matrix_error.error;
|
||||
const int status_code = static_cast<int>(err->status_code);
|
||||
|
||||
nhlog::net()->warn(
|
||||
"failed to create room: {} {} ({})", error, err_code, status_code);
|
||||
nhlog::net()->warn("failed to create room: {} {} ({})", error, err_code, status_code);
|
||||
|
||||
emit showNotification(
|
||||
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());
|
||||
emit showNotification(tr("Room %1 created.").arg(newRoomId));
|
||||
emit newRoom(newRoomId);
|
||||
emit changeToRoom(newRoomId);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -759,8 +752,7 @@ ChatPage::leaveRoom(const QString &room_id)
|
|||
room_id.toStdString(),
|
||||
[this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
emit showNotification(
|
||||
tr("Failed to leave room: %1")
|
||||
emit showNotification(tr("Failed to leave room: %1")
|
||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||
return;
|
||||
}
|
||||
|
@ -792,8 +784,7 @@ ChatPage::inviteUser(QString userid, QString reason)
|
|||
userid.toStdString(),
|
||||
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
emit showNotification(
|
||||
tr("Failed to invite %1 to %2: %3")
|
||||
emit showNotification(tr("Failed to invite %1 to %2: %3")
|
||||
.arg(userid)
|
||||
.arg(room)
|
||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||
|
@ -819,8 +810,7 @@ ChatPage::kickUser(QString userid, QString reason)
|
|||
userid.toStdString(),
|
||||
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
emit showNotification(
|
||||
tr("Failed to kick %1 from %2: %3")
|
||||
emit showNotification(tr("Failed to kick %1 from %2: %3")
|
||||
.arg(userid)
|
||||
.arg(room)
|
||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||
|
@ -846,8 +836,7 @@ ChatPage::banUser(QString userid, QString reason)
|
|||
userid.toStdString(),
|
||||
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
emit showNotification(
|
||||
tr("Failed to ban %1 in %2: %3")
|
||||
emit showNotification(tr("Failed to ban %1 in %2: %3")
|
||||
.arg(userid)
|
||||
.arg(room)
|
||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||
|
@ -873,8 +862,7 @@ ChatPage::unbanUser(QString userid, QString reason)
|
|||
userid.toStdString(),
|
||||
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
emit showNotification(
|
||||
tr("Failed to unban %1 in %2: %3")
|
||||
emit showNotification(tr("Failed to unban %1 in %2: %3")
|
||||
.arg(userid)
|
||||
.arg(room)
|
||||
.arg(QString::fromStdString(err->matrix_error.error)));
|
||||
|
@ -902,8 +890,7 @@ ChatPage::setStatus(const QString &status)
|
|||
http::client()->put_presence_status(
|
||||
currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->warn("failed to set presence status_msg: {}",
|
||||
err->matrix_error.error);
|
||||
nhlog::net()->warn("failed to set presence status_msg: {}", err->matrix_error.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -924,17 +911,49 @@ ChatPage::currentPresence() const
|
|||
}
|
||||
|
||||
void
|
||||
ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
|
||||
ChatPage::verifyOneTimeKeyCountAfterStartup()
|
||||
{
|
||||
uint16_t count = 0;
|
||||
if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end())
|
||||
count = c->second;
|
||||
http::client()->upload_keys(
|
||||
olm::client()->create_upload_keys_request(),
|
||||
[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) {
|
||||
const int nkeys = MAX_ONETIME_KEYS - count;
|
||||
if (err->status_code < 400 || err->status_code >= 500)
|
||||
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(
|
||||
"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);
|
||||
|
||||
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.
|
||||
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
|
||||
ChatPage::initiateLogout()
|
||||
{
|
||||
|
@ -1012,8 +1101,7 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
|
|||
QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
|
||||
keyDesc.name.empty()
|
||||
? QCoreApplication::translate(
|
||||
"CrossSigningSecrets",
|
||||
"Enter your recovery key or passphrase to decrypt your secrets:")
|
||||
"CrossSigningSecrets", "Enter your recovery key or passphrase to decrypt your secrets:")
|
||||
: QCoreApplication::translate(
|
||||
"CrossSigningSecrets",
|
||||
"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) {
|
||||
try {
|
||||
decryptionKey =
|
||||
mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
|
||||
decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
|
||||
} catch (std::exception &e) {
|
||||
nhlog::crypto()->error("Failed to derive secret key from passphrase: {}",
|
||||
e.what());
|
||||
nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1045,11 +1131,71 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
|
|||
return;
|
||||
}
|
||||
|
||||
auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
|
||||
mtx::requests::KeySignaturesUpload req;
|
||||
|
||||
for (const auto &[secretName, encryptedSecret] : secrets) {
|
||||
auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
|
||||
if (!decrypted.empty())
|
||||
if (!decrypted.empty()) {
|
||||
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
|
||||
|
@ -1061,11 +1207,9 @@ ChatPage::startChat(QString userid)
|
|||
for (std::string room_id : joined_rooms) {
|
||||
if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
|
||||
auto room_members = cache::roomMembers(room_id);
|
||||
if (std::find(room_members.begin(),
|
||||
room_members.end(),
|
||||
(userid).toStdString()) != room_members.end()) {
|
||||
view_manager_->rooms()->setCurrentRoom(
|
||||
QString::fromStdString(room_id));
|
||||
if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) !=
|
||||
room_members.end()) {
|
||||
view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1109,14 +1253,54 @@ mxidFromSegments(QStringRef sigil, QStringRef mxid)
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
ChatPage::handleMatrixUri(const QByteArray &uri)
|
||||
{
|
||||
nhlog::ui()->info("Received uri! {}", uri.toStdString());
|
||||
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")
|
||||
return;
|
||||
return false;
|
||||
|
||||
auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
|
||||
if (tempPath.startsWith('/'))
|
||||
|
@ -1124,17 +1308,17 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||
auto segments = tempPath.splitRef('/');
|
||||
|
||||
if (segments.size() != 2 && segments.size() != 4)
|
||||
return;
|
||||
return false;
|
||||
|
||||
auto sigil1 = segments[0];
|
||||
auto mxid1 = mxidFromSegments(sigil1, segments[1]);
|
||||
if (mxid1.isEmpty())
|
||||
return;
|
||||
return false;
|
||||
|
||||
QString mxid2;
|
||||
if (segments.size() == 4 && segments[2] == "e") {
|
||||
if (segments[3].isEmpty())
|
||||
return;
|
||||
return false;
|
||||
else
|
||||
mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
|
||||
}
|
||||
|
@ -1148,18 +1332,22 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||
if (item.startsWith("action=")) {
|
||||
action = item.remove("action=");
|
||||
} else if (item.startsWith("via=")) {
|
||||
vias.push_back(
|
||||
QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
|
||||
vias.push_back(QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
if (sigil1 == "u") {
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
emit view_manager_->openGlobalUserProfile(mxid1);
|
||||
} else if (action == "chat") {
|
||||
this->startChat(mxid1);
|
||||
}
|
||||
return true;
|
||||
} else if (sigil1 == "roomid") {
|
||||
auto joined_rooms = cache::joinedRooms();
|
||||
auto targetRoomId = mxid1.toStdString();
|
||||
|
@ -1169,13 +1357,15 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||
view_manager_->rooms()->setCurrentRoom(mxid1);
|
||||
if (!mxid2.isEmpty())
|
||||
view_manager_->showEvent(mxid1, mxid2);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (action == "join" || action.isEmpty()) {
|
||||
joinRoomVia(targetRoomId, vias);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (sigil1 == "r") {
|
||||
auto joined_rooms = cache::joinedRooms();
|
||||
auto targetRoomAlias = mxid1.toStdString();
|
||||
|
@ -1184,26 +1374,27 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
|
|||
auto aliases = cache::client()->getRoomAliases(roomid);
|
||||
if (aliases) {
|
||||
if (aliases->alias == targetRoomAlias) {
|
||||
view_manager_->rooms()->setCurrentRoom(
|
||||
QString::fromStdString(roomid));
|
||||
view_manager_->rooms()->setCurrentRoom(QString::fromStdString(roomid));
|
||||
if (!mxid2.isEmpty())
|
||||
view_manager_->showEvent(
|
||||
QString::fromStdString(roomid), mxid2);
|
||||
return;
|
||||
view_manager_->showEvent(QString::fromStdString(roomid), mxid2);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action == "join" || action.isEmpty()) {
|
||||
joinRoomVia(mxid1.toStdString(), vias);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
ChatPage::handleMatrixUri(const QUrl &uri)
|
||||
{
|
||||
handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
|
||||
return handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
|
||||
}
|
||||
|
||||
bool
|
||||
|
|
|
@ -78,8 +78,8 @@ public:
|
|||
QString currentRoom() const;
|
||||
|
||||
public slots:
|
||||
void handleMatrixUri(const QByteArray &uri);
|
||||
void handleMatrixUri(const QUrl &uri);
|
||||
bool handleMatrixUri(const QByteArray &uri);
|
||||
bool handleMatrixUri(const QUrl &uri);
|
||||
|
||||
void startChat(QString userid);
|
||||
void leaveRoom(const QString &room_id);
|
||||
|
@ -102,8 +102,7 @@ signals:
|
|||
void connectionRestored();
|
||||
|
||||
void notificationsRetrieved(const mtx::responses::Notifications &);
|
||||
void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
|
||||
const QPoint widgetPos);
|
||||
void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos);
|
||||
|
||||
void contentLoaded();
|
||||
void closing();
|
||||
|
@ -125,6 +124,7 @@ signals:
|
|||
void newSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token);
|
||||
void leftRoom(const QString &room_id);
|
||||
void newRoom(const QString &room_id);
|
||||
void changeToRoom(const QString &room_id);
|
||||
|
||||
void initializeViews(const mtx::responses::Rooms &rooms);
|
||||
void initializeEmptyViews();
|
||||
|
@ -145,16 +145,13 @@ signals:
|
|||
void chatFocusChanged(const bool focused);
|
||||
|
||||
//! Signals for device verificaiton
|
||||
void receivedDeviceVerificationAccept(
|
||||
const mtx::events::msg::KeyVerificationAccept &message);
|
||||
void receivedDeviceVerificationRequest(
|
||||
const mtx::events::msg::KeyVerificationRequest &message,
|
||||
void receivedDeviceVerificationAccept(const mtx::events::msg::KeyVerificationAccept &message);
|
||||
void receivedDeviceVerificationRequest(const mtx::events::msg::KeyVerificationRequest &message,
|
||||
std::string sender);
|
||||
void receivedRoomDeviceVerificationRequest(
|
||||
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
|
||||
TimelineModel *model);
|
||||
void receivedDeviceVerificationCancel(
|
||||
const mtx::events::msg::KeyVerificationCancel &message);
|
||||
void receivedDeviceVerificationCancel(const mtx::events::msg::KeyVerificationCancel &message);
|
||||
void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
|
||||
void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
|
||||
void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
|
||||
|
@ -171,8 +168,7 @@ private slots:
|
|||
void changeRoom(const QString &room_id);
|
||||
void dropToLoginPage(const QString &msg);
|
||||
|
||||
void handleSyncResponse(const mtx::responses::Sync &res,
|
||||
const std::string &prev_batch_token);
|
||||
void handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token);
|
||||
|
||||
private:
|
||||
static ChatPage *instance_;
|
||||
|
@ -180,8 +176,10 @@ private:
|
|||
void startInitialSync();
|
||||
void tryInitialSync();
|
||||
void trySync();
|
||||
void verifyOneTimeKeyCountAfterStartup();
|
||||
void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
|
||||
void getProfileInfo();
|
||||
void getBackupVersion();
|
||||
|
||||
//! Check if the given room is currently open.
|
||||
bool isRoomActive(const QString &room_id);
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
Clipboard::Clipboard(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(
|
||||
QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged);
|
||||
connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -99,8 +99,8 @@ int
|
|||
CompletionProxyModel::rowCount(const QModelIndex &) const
|
||||
{
|
||||
if (searchString_.isEmpty())
|
||||
return std::min(static_cast<int>(std::min<size_t>(max_completions_,
|
||||
std::numeric_limits<int>::max())),
|
||||
return std::min(
|
||||
static_cast<int>(std::min<size_t>(max_completions_, std::numeric_limits<int>::max())),
|
||||
sourceModel()->rowCount());
|
||||
else
|
||||
return (int)mapping.size();
|
||||
|
|
|
@ -94,8 +94,7 @@ struct trie
|
|||
if (keys.size() >= 2) {
|
||||
auto t = this;
|
||||
for (int i = 1; i >= 0; i--) {
|
||||
if (auto e = t->next.find(keys[i]);
|
||||
e != t->next.end()) {
|
||||
if (auto e = t->next.find(keys[i]); e != t->next.end()) {
|
||||
t = &e->second;
|
||||
} else {
|
||||
t = nullptr;
|
||||
|
@ -104,8 +103,7 @@ struct trie
|
|||
}
|
||||
|
||||
if (t) {
|
||||
append(t->search(
|
||||
keys.mid(2), limit(), max_edit_distance));
|
||||
append(t->search(keys.mid(2), limit(), max_edit_distance));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,8 +147,7 @@ struct trie
|
|||
class CompletionProxyModel : public QAbstractProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(
|
||||
QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
|
||||
Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
|
||||
public:
|
||||
CompletionProxyModel(QAbstractItemModel *model,
|
||||
int max_mistakes = 2,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
};
|
|
@ -39,8 +39,7 @@ struct EventMsgType
|
|||
if constexpr (std::is_same_v<std::optional<std::string>,
|
||||
std::remove_cv_t<decltype(e.content.msgtype)>>)
|
||||
return mtx::events::getMessageType(e.content.msgtype.value());
|
||||
else if constexpr (std::is_same_v<
|
||||
std::string,
|
||||
else if constexpr (std::is_same_v<std::string,
|
||||
std::remove_cv_t<decltype(e.content.msgtype)>>)
|
||||
return mtx::events::getMessageType(e.content.msgtype);
|
||||
}
|
||||
|
@ -75,8 +74,7 @@ struct CallType
|
|||
template<class T>
|
||||
std::string operator()(const T &e)
|
||||
{
|
||||
if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>,
|
||||
T>) {
|
||||
if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, T>) {
|
||||
const char video[] = "m=video";
|
||||
const std::string &sdp = e.content.sdp;
|
||||
return std::search(sdp.cbegin(),
|
||||
|
@ -104,8 +102,7 @@ struct EventBody
|
|||
if constexpr (std::is_same_v<std::optional<std::string>,
|
||||
std::remove_cv_t<decltype(e.content.body)>>)
|
||||
return e.content.body ? e.content.body.value() : "";
|
||||
else if constexpr (std::is_same_v<
|
||||
std::string,
|
||||
else if constexpr (std::is_same_v<std::string,
|
||||
std::remove_cv_t<decltype(e.content.body)>>)
|
||||
return e.content.body;
|
||||
}
|
||||
|
|
|
@ -16,8 +16,7 @@ ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *paren
|
|||
auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt);
|
||||
|
||||
for (const auto &pack : packs_) {
|
||||
packs.push_back(
|
||||
QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
|
||||
packs.push_back(QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue