Update video_player_enhancements with changes from master

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

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

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

View file

@ -1,14 +1,14 @@
---
Language: Cpp
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

View file

@ -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

View file

@ -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,31 +326,38 @@ 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/ReadReceiptsModel.cpp
src/RegisterPage.cpp
src/SSOHandler.cpp
src/CombinedImagePackModel.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)

View file

@ -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)

View file

@ -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:

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

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

After

Width:  |  Height:  |  Size: 1 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3325
resources/langs/nheko_id.ts Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
<component type="desktop">
<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>

View file

@ -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

View file

@ -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
}
}

View file

@ -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 {
}
]
TapHandler {
margin: -Nheko.paddingSmall
acceptedButtons: Qt.RightButton
onSingleTapped: communityContextMenu.show(model.id)
gesturePolicy: TapHandler.ReleaseWithinBounds
Item {
anchors.fill: parent
TapHandler {
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 {

View file

@ -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))
}

View file

@ -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.");
}
}

View file

@ -80,15 +80,15 @@ 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) {
} else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
completerPopup.up();
else
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion();
event.accepted = true;

View file

@ -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,7 +195,10 @@ Rectangle {
} else if (event.key == Qt.Key_Tab) {
event.accepted = true;
if (popup.opened) {
popup.up();
if (event.modifiers & Qt.ShiftModifier)
popup.down();
else
popup.up();
} else {
var pos = cursorPosition - 1;
while (pos > -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 {

View file

@ -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)
}
}
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import im.nheko 1.0
Item {
implicitHeight: warningRect.visible ? warningDisplay.implicitHeight : 0
height: implicitHeight
Layout.fillWidth: true
Rectangle {
id: warningRect
visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
color: Nheko.colors.base
anchors.fill: parent
z: 3
Label {
id: warningDisplay
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
anchors.bottom: parent.bottom
color: Nheko.theme.red
text: qsTr("You are about to notify the whole room")
textFormat: Text.PlainText
}
}
}

View file

@ -39,15 +39,15 @@ Popup {
completerPopup.completer.searchString = text;
}
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) {
} else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Tab) && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
completerPopup.up();
else
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion();
event.accepted = true;

View file

@ -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 {

View file

@ -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
}

View file

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

View file

@ -24,7 +24,7 @@ Item {
property bool showBackButton: false
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
MessageView {
implicitHeight: msgView.height - typingIndicator.height
Layout.fillWidth: true
sourceComponent: MessageView {
implicitHeight: msgView.height - typingIndicator.height
}
}
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

View file

@ -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)
}
}

View file

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

View file

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

View file

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

View file

@ -3,11 +3,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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

View file

@ -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,38 +55,48 @@ Item {
gesturePolicy: TapHandler.ReleaseWithinBounds
}
HoverHandler {
id: mouseArea
}
MxcAnimatedImage {
id: mxcimage
visible: loaded
anchors.fill: parent
roomm: room
play: !Settings.animateImagesOnHover || mouseArea.hovered
eventId: parent.eventId
}
HoverHandler {
id: mouseArea
}
Item {
id: overlay
anchors.fill: parent
visible: mouseArea.hovered
Rectangle {
id: container
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: Nheko.colors.window
opacity: 0.75
}
Item {
id: overlay
anchors.fill: parent
visible: mouseArea.hovered
Rectangle {
id: container
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: Nheko.colors.window
opacity: 0.75
}
Text {
id: imgcaption
anchors.fill: container
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: filename ? filename : body
color: Nheko.colors.text
}
Text {
id: imgcaption
anchors.fill: container
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: filename ? filename : body
color: Nheko.colors.text
}
}

View file

@ -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,11 +361,23 @@ Item {
DelegateChoice {
roleValue: MtxEvent.Member
NoticeMessage {
body: formatted
isOnlyEmoji: false
isReply: d.isReply
formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId)
ColumnLayout {
width: parent ? parent.width : undefined
NoticeMessage {
body: formatted
isOnlyEmoji: false
isReply: d.isReply
formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId)
}
Button {
visible: d.relatedEventCacheBuster, room.showAcceptKnockButton(d.eventId)
palette: Nheko.colors
text: qsTr("Allow them in")
onClicked: room.acceptKnock(eventId)
}
}
}

View file

@ -3,9 +3,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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,26 +40,16 @@ 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
}
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)
}
onError: console.log(error)
roomm: room
onMediaStatusChanged: {
if (status == MxcMedia.LoadedMedia) {
progress.updatePositionTexts();
}
}
}
Rectangle {
@ -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
}
}
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ ApplicationWindow {
width: 420
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 {

View file

@ -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 {

View file

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

View file

@ -2,7 +2,8 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
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/")

View file

@ -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);

View file

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

View file

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

View file

@ -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"
}

View file

@ -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 {

View file

@ -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"
}

View file

@ -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)
}

View file

@ -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>

View 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()

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -83,6 +83,9 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
//! Retrieve member info from a room.
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();

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

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