mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 20:48:52 +03:00
Update video_player_enhancements with changes from master
This commit is contained in:
commit
743a83c8e6
265 changed files with 53777 additions and 34019 deletions
73
.ci/macos/notarize.sh
Executable file
73
.ci/macos/notarize.sh
Executable file
|
@ -0,0 +1,73 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -u
|
||||
|
||||
# Modified version of script found at:
|
||||
# https://forum.qt.io/topic/96652/how-to-notarize-qt-application-on-macos/18
|
||||
|
||||
# Add Qt binaries to path
|
||||
PATH="/usr/local/opt/qt@5/bin/:${PATH}"
|
||||
|
||||
security unlock-keychain -p "${RUNNER_USER_PW}" login.keychain
|
||||
|
||||
( cd build || exit
|
||||
# macdeployqt does not copy symlinks over.
|
||||
# this specifically addresses icu4c issues but nothing else.
|
||||
# We might not even need this any longer...
|
||||
# ICU_LIB="$(brew --prefix icu4c)/lib"
|
||||
# export ICU_LIB
|
||||
# mkdir -p nheko.app/Contents/Frameworks
|
||||
# find "${ICU_LIB}" -type l -name "*.dylib" -exec cp -a -n {} nheko.app/Contents/Frameworks/ \; || true
|
||||
|
||||
macdeployqt nheko.app -dmg -always-overwrite -qmldir=../resources/qml/ -sign-for-notarization="${APPLE_DEV_IDENTITY}"
|
||||
|
||||
user=$(id -nu)
|
||||
chown "${user}" nheko.dmg
|
||||
)
|
||||
|
||||
NOTARIZE_SUBMIT_LOG=$(mktemp -t notarize-submit)
|
||||
NOTARIZE_STATUS_LOG=$(mktemp -t notarize-status)
|
||||
|
||||
finish() {
|
||||
rm "$NOTARIZE_SUBMIT_LOG" "$NOTARIZE_STATUS_LOG"
|
||||
}
|
||||
trap finish EXIT
|
||||
|
||||
dmgbuild -s .ci/macos/settings.json "Nheko" nheko.dmg
|
||||
codesign -s "${APPLE_DEV_IDENTITY}" nheko.dmg
|
||||
user=$(id -nu)
|
||||
chown "${user}" nheko.dmg
|
||||
|
||||
echo "--> Start Notarization process"
|
||||
xcrun altool -t osx -f nheko.dmg --primary-bundle-id "io.github.nheko-reborn.nheko" --notarize-app -u "${APPLE_DEV_USER}" -p "${APPLE_DEV_PASS}" > "$NOTARIZE_SUBMIT_LOG" 2>&1
|
||||
requestUUID="$(awk -F ' = ' '/RequestUUID/ {print $2}' "$NOTARIZE_SUBMIT_LOG")"
|
||||
|
||||
while sleep 60 && date; do
|
||||
echo "--> Checking notarization status for ${requestUUID}"
|
||||
|
||||
xcrun altool --notarization-info "${requestUUID}" -u "${APPLE_DEV_USER}" -p "${APPLE_DEV_PASS}" > "$NOTARIZE_STATUS_LOG" 2>&1
|
||||
|
||||
isSuccess=$(grep "success" "$NOTARIZE_STATUS_LOG")
|
||||
isFailure=$(grep "invalid" "$NOTARIZE_STATUS_LOG")
|
||||
|
||||
if [ -n "${isSuccess}" ]; then
|
||||
echo "Notarization done!"
|
||||
xcrun stapler staple -v nheko.dmg
|
||||
echo "Stapler done!"
|
||||
break
|
||||
fi
|
||||
if [ -n "${isFailure}" ]; then
|
||||
echo "Notarization failed"
|
||||
cat "$NOTARIZE_STATUS_LOG" 1>&2
|
||||
return 1
|
||||
fi
|
||||
echo "Notarization not finished yet, sleep 1m then check again..."
|
||||
done
|
||||
|
||||
VERSION=${CI_COMMIT_SHORT_SHA}
|
||||
|
||||
if [ -n "$VERSION" ]; then
|
||||
mv nheko.dmg "nheko-${VERSION}.dmg"
|
||||
mkdir artifacts
|
||||
cp "nheko-${VERSION}.dmg" artifacts/
|
||||
fi
|
|
@ -1,14 +1,14 @@
|
|||
---
|
||||
Language: Cpp
|
||||
Standard: Cpp11
|
||||
AccessModifierOffset: -8
|
||||
Standard: c++17
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: true
|
||||
AllowShortFunctionsOnASingleLine: true
|
||||
BasedOnStyle: Mozilla
|
||||
ColumnLimit: 100
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 8
|
||||
IndentWidth: 4
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
PointerAlignment: Right
|
||||
Cpp11BracedListStyle: true
|
||||
|
|
|
@ -55,7 +55,6 @@ build-macos:
|
|||
#- brew update
|
||||
#- brew reinstall --force python3
|
||||
#- brew bundle --file=./.ci/macos/Brewfile --force --cleanup
|
||||
- pip3 install dmgbuild
|
||||
- rm -rf ../.hunter && mv .hunter ../.hunter || true
|
||||
script:
|
||||
- export PATH=/usr/local/opt/qt@5/bin/:${PATH}
|
||||
|
@ -72,19 +71,40 @@ build-macos:
|
|||
- cmake --build build
|
||||
after_script:
|
||||
- mv ../.hunter .hunter
|
||||
- ./.ci/macos/deploy.sh
|
||||
- ./.ci/upload-nightly-gitlab.sh artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
|
||||
name: nheko-${CI_COMMIT_SHORT_SHA}-macos
|
||||
expose_as: 'macos-dmg'
|
||||
- build/nheko.app
|
||||
name: nheko-${CI_COMMIT_SHORT_SHA}-macos-app
|
||||
expose_as: 'macos-app'
|
||||
public: false
|
||||
cache:
|
||||
key: "${CI_JOB_NAME}"
|
||||
paths:
|
||||
- .hunter/
|
||||
- "${CCACHE_DIR}"
|
||||
|
||||
codesign-macos:
|
||||
stage: deploy
|
||||
tags: [macos]
|
||||
before_script:
|
||||
- pip3 install dmgbuild
|
||||
script:
|
||||
- export PATH=/usr/local/opt/qt@5/bin/:${PATH}
|
||||
- ./.ci/macos/notarize.sh
|
||||
after_script:
|
||||
- ./.ci/upload-nightly-gitlab.sh artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
|
||||
needs:
|
||||
- build-macos
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
- if : $CI_COMMIT_TAG
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/nheko-${CI_COMMIT_SHORT_SHA}.dmg
|
||||
name: nheko-${CI_COMMIT_SHORT_SHA}-macos
|
||||
expose_as: 'macos-dmg'
|
||||
|
||||
|
||||
build-flatpak-amd64:
|
||||
stage: build
|
||||
image: ubuntu:latest
|
||||
|
@ -171,7 +191,7 @@ appimage-amd64:
|
|||
- apt-get install -y git wget curl
|
||||
|
||||
# update appimage-builder (optional)
|
||||
- pip3 install --upgrade git+https://www.opencode.net/azubieta/appimagecraft.git
|
||||
- pip3 install --upgrade git+https://github.com/AppImageCrafters/appimage-builder.git
|
||||
|
||||
- apt-get update && apt-get -y install --no-install-recommends g++-7 build-essential ninja-build qt${QT_PKG}{base,declarative,tools,multimedia,script,quickcontrols2,svg} liblmdb-dev libssl-dev git ninja-build qt5keychain-dev libgtest-dev ccache libevent-dev libcurl4-openssl-dev libgl1-mesa-dev
|
||||
- wget https://github.com/Kitware/CMake/releases/download/v3.19.0/cmake-3.19.0-Linux-x86_64.sh && sh cmake-3.19.0-Linux-x86_64.sh --skip-license --prefix=/usr/local
|
||||
|
|
|
@ -281,8 +281,6 @@ set(SRC_FILES
|
|||
src/dialogs/CreateRoom.cpp
|
||||
src/dialogs/FallbackAuth.cpp
|
||||
src/dialogs/ImageOverlay.cpp
|
||||
src/dialogs/JoinRoom.cpp
|
||||
src/dialogs/LeaveRoom.cpp
|
||||
src/dialogs/Logout.cpp
|
||||
src/dialogs/PreviewUploadOverlay.cpp
|
||||
src/dialogs/ReCaptcha.cpp
|
||||
|
@ -311,6 +309,8 @@ set(SRC_FILES
|
|||
src/ui/InfoMessage.cpp
|
||||
src/ui/Label.cpp
|
||||
src/ui/LoadingIndicator.cpp
|
||||
src/ui/MxcAnimatedImage.cpp
|
||||
src/ui/MxcMediaProxy.cpp
|
||||
src/ui/NhekoCursorShape.cpp
|
||||
src/ui/NhekoDropArea.cpp
|
||||
src/ui/NhekoGlobalObject.cpp
|
||||
|
@ -326,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)
|
||||
|
|
27
README.md
27
README.md
|
@ -77,6 +77,7 @@ sudo dnf install nheko
|
|||
#### Gentoo Linux
|
||||
```bash
|
||||
sudo eselect repository enable guru
|
||||
sudo emaint sync -r guru
|
||||
sudo emerge -a nheko
|
||||
```
|
||||
|
||||
|
@ -126,11 +127,31 @@ choco install nheko-reborn
|
|||
|
||||
### FAQ
|
||||
|
||||
##
|
||||
---
|
||||
|
||||
**Q:** Why don't videos run for me on Windows?
|
||||
|
||||
**A:** You're probably missing the required video codecs, download [K-Lite Codec Pack](https://codecguide.com/download_kl.htm).
|
||||
##
|
||||
|
||||
---
|
||||
|
||||
**Q:** What commands are supported by nheko?
|
||||
|
||||
**A:** See <https://github.com/Nheko-Reborn/nheko/wiki/Commands>
|
||||
|
||||
---
|
||||
|
||||
**Q:** Does nheko support end-to-end encryption (EE2E)?
|
||||
|
||||
**A:** Yes, see [feature list](#features)
|
||||
|
||||
---
|
||||
|
||||
**Q:** Can I test a bleeding edge development version?
|
||||
|
||||
**A:** Checkout nightly builds <https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/>
|
||||
|
||||
---
|
||||
|
||||
### Build Requirements
|
||||
|
||||
|
@ -150,7 +171,7 @@ choco install nheko-reborn
|
|||
- Voice call support: dtls, opus, rtpmanager, srtp, webrtc
|
||||
- Video call support (optional): compositor, opengl, qmlgl, rtp, vpx
|
||||
- [libnice](https://gitlab.freedesktop.org/libnice/libnice)
|
||||
- [qtkeychain](https://github.com/frankosterfeld/qtkeychain)
|
||||
- [qtkeychain](https://github.com/frankosterfeld/qtkeychain) (You need at least version 0.12 for proper Gnome Keychain support)
|
||||
- A compiler that supports C++ 17:
|
||||
- Clang 6 (tested on Travis CI)
|
||||
- GCC 7 (tested on Travis CI)
|
||||
|
|
|
@ -163,7 +163,7 @@ modules:
|
|||
buildsystem: cmake-ninja
|
||||
name: mtxclient
|
||||
sources:
|
||||
- commit: deb51ef1d6df870098069312f0a1999550e1eb85
|
||||
- commit: 7fe7a70fcf7540beb6d7b4847e53a425de66c6bf
|
||||
type: git
|
||||
url: https://github.com/Nheko-Reborn/mtxclient.git
|
||||
- config-opts:
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# emoji-test.txt
|
||||
# Date: 2020-09-12, 22:19:50 GMT
|
||||
# © 2020 Unicode®, Inc.
|
||||
# Date: 2021-08-26, 17:22:23 GMT
|
||||
# © 2021 Unicode®, Inc.
|
||||
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
|
||||
# For terms of use, see http://www.unicode.org/terms_of_use.html
|
||||
#
|
||||
# Emoji Keyboard/Display Test Data for UTS #51
|
||||
# Version: 13.1
|
||||
# Version: 14.0
|
||||
#
|
||||
# For documentation and usage, see http://www.unicode.org/reports/tr51
|
||||
#
|
||||
|
@ -43,6 +43,7 @@
|
|||
1F602 ; fully-qualified # 😂 E0.6 face with tears of joy
|
||||
1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face
|
||||
1F643 ; fully-qualified # 🙃 E1.0 upside-down face
|
||||
1FAE0 ; fully-qualified # 🫠 E14.0 melting face
|
||||
1F609 ; fully-qualified # 😉 E0.6 winking face
|
||||
1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes
|
||||
1F607 ; fully-qualified # 😇 E1.0 smiling face with halo
|
||||
|
@ -68,10 +69,13 @@
|
|||
1F911 ; fully-qualified # 🤑 E1.0 money-mouth face
|
||||
|
||||
# subgroup: face-hand
|
||||
1F917 ; fully-qualified # 🤗 E1.0 hugging face
|
||||
1F917 ; fully-qualified # 🤗 E1.0 smiling face with open hands
|
||||
1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth
|
||||
1FAE2 ; fully-qualified # 🫢 E14.0 face with open eyes and hand over mouth
|
||||
1FAE3 ; fully-qualified # 🫣 E14.0 face with peeking eye
|
||||
1F92B ; fully-qualified # 🤫 E5.0 shushing face
|
||||
1F914 ; fully-qualified # 🤔 E1.0 thinking face
|
||||
1FAE1 ; fully-qualified # 🫡 E14.0 saluting face
|
||||
|
||||
# subgroup: face-neutral-skeptical
|
||||
1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face
|
||||
|
@ -79,6 +83,7 @@
|
|||
1F610 ; fully-qualified # 😐 E0.7 neutral face
|
||||
1F611 ; fully-qualified # 😑 E1.0 expressionless face
|
||||
1F636 ; fully-qualified # 😶 E1.0 face without mouth
|
||||
1FAE5 ; fully-qualified # 🫥 E14.0 dotted line face
|
||||
1F636 200D 1F32B FE0F ; fully-qualified # 😶🌫️ E13.1 face in clouds
|
||||
1F636 200D 1F32B ; minimally-qualified # 😶🌫 E13.1 face in clouds
|
||||
1F60F ; fully-qualified # 😏 E0.6 smirking face
|
||||
|
@ -105,7 +110,7 @@
|
|||
1F975 ; fully-qualified # 🥵 E11.0 hot face
|
||||
1F976 ; fully-qualified # 🥶 E11.0 cold face
|
||||
1F974 ; fully-qualified # 🥴 E11.0 woozy face
|
||||
1F635 ; fully-qualified # 😵 E0.6 knocked-out face
|
||||
1F635 ; fully-qualified # 😵 E0.6 face with crossed-out eyes
|
||||
1F635 200D 1F4AB ; fully-qualified # 😵💫 E13.1 face with spiral eyes
|
||||
1F92F ; fully-qualified # 🤯 E5.0 exploding head
|
||||
|
||||
|
@ -121,6 +126,7 @@
|
|||
|
||||
# subgroup: face-concerned
|
||||
1F615 ; fully-qualified # 😕 E1.0 confused face
|
||||
1FAE4 ; fully-qualified # 🫤 E14.0 face with diagonal mouth
|
||||
1F61F ; fully-qualified # 😟 E1.0 worried face
|
||||
1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face
|
||||
2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face
|
||||
|
@ -130,6 +136,7 @@
|
|||
1F632 ; fully-qualified # 😲 E0.6 astonished face
|
||||
1F633 ; fully-qualified # 😳 E0.6 flushed face
|
||||
1F97A ; fully-qualified # 🥺 E11.0 pleading face
|
||||
1F979 ; fully-qualified # 🥹 E14.0 face holding back tears
|
||||
1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth
|
||||
1F627 ; fully-qualified # 😧 E1.0 anguished face
|
||||
1F628 ; fully-qualified # 😨 E0.6 fearful face
|
||||
|
@ -232,8 +239,8 @@
|
|||
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
|
||||
1F4A4 ; fully-qualified # 💤 E0.6 zzz
|
||||
|
||||
# Smileys & Emotion subtotal: 170
|
||||
# Smileys & Emotion subtotal: 170 w/o modifiers
|
||||
# Smileys & Emotion subtotal: 177
|
||||
# Smileys & Emotion subtotal: 177 w/o modifiers
|
||||
|
||||
# group: People & Body
|
||||
|
||||
|
@ -269,6 +276,30 @@
|
|||
1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone
|
||||
1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
|
||||
1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone
|
||||
1FAF1 ; fully-qualified # 🫱 E14.0 rightwards hand
|
||||
1FAF1 1F3FB ; fully-qualified # 🫱🏻 E14.0 rightwards hand: light skin tone
|
||||
1FAF1 1F3FC ; fully-qualified # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
|
||||
1FAF1 1F3FD ; fully-qualified # 🫱🏽 E14.0 rightwards hand: medium skin tone
|
||||
1FAF1 1F3FE ; fully-qualified # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
|
||||
1FAF1 1F3FF ; fully-qualified # 🫱🏿 E14.0 rightwards hand: dark skin tone
|
||||
1FAF2 ; fully-qualified # 🫲 E14.0 leftwards hand
|
||||
1FAF2 1F3FB ; fully-qualified # 🫲🏻 E14.0 leftwards hand: light skin tone
|
||||
1FAF2 1F3FC ; fully-qualified # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
|
||||
1FAF2 1F3FD ; fully-qualified # 🫲🏽 E14.0 leftwards hand: medium skin tone
|
||||
1FAF2 1F3FE ; fully-qualified # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
|
||||
1FAF2 1F3FF ; fully-qualified # 🫲🏿 E14.0 leftwards hand: dark skin tone
|
||||
1FAF3 ; fully-qualified # 🫳 E14.0 palm down hand
|
||||
1FAF3 1F3FB ; fully-qualified # 🫳🏻 E14.0 palm down hand: light skin tone
|
||||
1FAF3 1F3FC ; fully-qualified # 🫳🏼 E14.0 palm down hand: medium-light skin tone
|
||||
1FAF3 1F3FD ; fully-qualified # 🫳🏽 E14.0 palm down hand: medium skin tone
|
||||
1FAF3 1F3FE ; fully-qualified # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
|
||||
1FAF3 1F3FF ; fully-qualified # 🫳🏿 E14.0 palm down hand: dark skin tone
|
||||
1FAF4 ; fully-qualified # 🫴 E14.0 palm up hand
|
||||
1FAF4 1F3FB ; fully-qualified # 🫴🏻 E14.0 palm up hand: light skin tone
|
||||
1FAF4 1F3FC ; fully-qualified # 🫴🏼 E14.0 palm up hand: medium-light skin tone
|
||||
1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone
|
||||
1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
|
||||
1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone
|
||||
|
||||
# subgroup: hand-fingers-partial
|
||||
1F44C ; fully-qualified # 👌 E0.6 OK hand
|
||||
|
@ -302,6 +333,12 @@
|
|||
1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone
|
||||
1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
|
||||
1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone
|
||||
1FAF0 ; fully-qualified # 🫰 E14.0 hand with index finger and thumb crossed
|
||||
1FAF0 1F3FB ; fully-qualified # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
|
||||
1FAF0 1F3FC ; fully-qualified # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
|
||||
1FAF0 1F3FD ; fully-qualified # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
|
||||
1FAF0 1F3FE ; fully-qualified # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
|
||||
1FAF0 1F3FF ; fully-qualified # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
|
||||
1F91F ; fully-qualified # 🤟 E5.0 love-you gesture
|
||||
1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone
|
||||
1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
|
||||
|
@ -359,6 +396,12 @@
|
|||
261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone
|
||||
261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone
|
||||
261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone
|
||||
1FAF5 ; fully-qualified # 🫵 E14.0 index pointing at the viewer
|
||||
1FAF5 1F3FB ; fully-qualified # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
|
||||
1FAF5 1F3FC ; fully-qualified # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
|
||||
1FAF5 1F3FD ; fully-qualified # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
|
||||
1FAF5 1F3FE ; fully-qualified # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
|
||||
1FAF5 1F3FF ; fully-qualified # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
|
||||
|
||||
# subgroup: hand-fingers-closed
|
||||
1F44D ; fully-qualified # 👍 E0.6 thumbs up
|
||||
|
@ -411,6 +454,12 @@
|
|||
1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone
|
||||
1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone
|
||||
1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone
|
||||
1FAF6 ; fully-qualified # 🫶 E14.0 heart hands
|
||||
1FAF6 1F3FB ; fully-qualified # 🫶🏻 E14.0 heart hands: light skin tone
|
||||
1FAF6 1F3FC ; fully-qualified # 🫶🏼 E14.0 heart hands: medium-light skin tone
|
||||
1FAF6 1F3FD ; fully-qualified # 🫶🏽 E14.0 heart hands: medium skin tone
|
||||
1FAF6 1F3FE ; fully-qualified # 🫶🏾 E14.0 heart hands: medium-dark skin tone
|
||||
1FAF6 1F3FF ; fully-qualified # 🫶🏿 E14.0 heart hands: dark skin tone
|
||||
1F450 ; fully-qualified # 👐 E0.6 open hands
|
||||
1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone
|
||||
1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone
|
||||
|
@ -424,6 +473,31 @@
|
|||
1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
|
||||
1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
|
||||
1F91D ; fully-qualified # 🤝 E3.0 handshake
|
||||
1F91D 1F3FB ; fully-qualified # 🤝🏻 E3.0 handshake: light skin tone
|
||||
1F91D 1F3FC ; fully-qualified # 🤝🏼 E3.0 handshake: medium-light skin tone
|
||||
1F91D 1F3FD ; fully-qualified # 🤝🏽 E3.0 handshake: medium skin tone
|
||||
1F91D 1F3FE ; fully-qualified # 🤝🏾 E3.0 handshake: medium-dark skin tone
|
||||
1F91D 1F3FF ; fully-qualified # 🤝🏿 E3.0 handshake: dark skin tone
|
||||
1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
|
||||
1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻🫲🏽 E14.0 handshake: light skin tone, medium skin tone
|
||||
1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
|
||||
1FAF1 1F3FB 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏻🫲🏿 E14.0 handshake: light skin tone, dark skin tone
|
||||
1FAF1 1F3FC 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏼🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
|
||||
1FAF1 1F3FC 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏼🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
|
||||
1FAF1 1F3FC 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏼🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
|
||||
1FAF1 1F3FC 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏼🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
|
||||
1FAF1 1F3FD 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏽🫲🏻 E14.0 handshake: medium skin tone, light skin tone
|
||||
1FAF1 1F3FD 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏽🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
|
||||
1FAF1 1F3FD 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏽🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
|
||||
1FAF1 1F3FD 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏽🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
|
||||
1FAF1 1F3FE 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏾🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
|
||||
1FAF1 1F3FE 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏾🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
|
||||
1FAF1 1F3FE 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏾🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
|
||||
1FAF1 1F3FE 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏾🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
|
||||
1FAF1 1F3FF 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏿🫲🏻 E14.0 handshake: dark skin tone, light skin tone
|
||||
1FAF1 1F3FF 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏿🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
|
||||
1FAF1 1F3FF 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏿🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
|
||||
1FAF1 1F3FF 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏿🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
|
||||
1F64F ; fully-qualified # 🙏 E0.6 folded hands
|
||||
1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone
|
||||
1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone
|
||||
|
@ -501,6 +575,7 @@
|
|||
1F441 ; unqualified # 👁 E0.7 eye
|
||||
1F445 ; fully-qualified # 👅 E0.6 tongue
|
||||
1F444 ; fully-qualified # 👄 E0.6 mouth
|
||||
1FAE6 ; fully-qualified # 🫦 E14.0 biting lip
|
||||
|
||||
# subgroup: person
|
||||
1F476 ; fully-qualified # 👶 E0.6 baby
|
||||
|
@ -1472,6 +1547,12 @@
|
|||
1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾♀ E4.0 woman construction worker: medium-dark skin tone
|
||||
1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿♀️ E4.0 woman construction worker: dark skin tone
|
||||
1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿♀ E4.0 woman construction worker: dark skin tone
|
||||
1FAC5 ; fully-qualified # 🫅 E14.0 person with crown
|
||||
1FAC5 1F3FB ; fully-qualified # 🫅🏻 E14.0 person with crown: light skin tone
|
||||
1FAC5 1F3FC ; fully-qualified # 🫅🏼 E14.0 person with crown: medium-light skin tone
|
||||
1FAC5 1F3FD ; fully-qualified # 🫅🏽 E14.0 person with crown: medium skin tone
|
||||
1FAC5 1F3FE ; fully-qualified # 🫅🏾 E14.0 person with crown: medium-dark skin tone
|
||||
1FAC5 1F3FF ; fully-qualified # 🫅🏿 E14.0 person with crown: dark skin tone
|
||||
1F934 ; fully-qualified # 🤴 E3.0 prince
|
||||
1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone
|
||||
1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone
|
||||
|
@ -1592,6 +1673,18 @@
|
|||
1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone
|
||||
1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
|
||||
1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone
|
||||
1FAC3 ; fully-qualified # 🫃 E14.0 pregnant man
|
||||
1FAC3 1F3FB ; fully-qualified # 🫃🏻 E14.0 pregnant man: light skin tone
|
||||
1FAC3 1F3FC ; fully-qualified # 🫃🏼 E14.0 pregnant man: medium-light skin tone
|
||||
1FAC3 1F3FD ; fully-qualified # 🫃🏽 E14.0 pregnant man: medium skin tone
|
||||
1FAC3 1F3FE ; fully-qualified # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
|
||||
1FAC3 1F3FF ; fully-qualified # 🫃🏿 E14.0 pregnant man: dark skin tone
|
||||
1FAC4 ; fully-qualified # 🫄 E14.0 pregnant person
|
||||
1FAC4 1F3FB ; fully-qualified # 🫄🏻 E14.0 pregnant person: light skin tone
|
||||
1FAC4 1F3FC ; fully-qualified # 🫄🏼 E14.0 pregnant person: medium-light skin tone
|
||||
1FAC4 1F3FD ; fully-qualified # 🫄🏽 E14.0 pregnant person: medium skin tone
|
||||
1FAC4 1F3FE ; fully-qualified # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
|
||||
1FAC4 1F3FF ; fully-qualified # 🫄🏿 E14.0 pregnant person: dark skin tone
|
||||
1F931 ; fully-qualified # 🤱 E5.0 breast-feeding
|
||||
1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone
|
||||
1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
|
||||
|
@ -1862,6 +1955,7 @@
|
|||
1F9DF 200D 2642 ; minimally-qualified # 🧟♂ E5.0 man zombie
|
||||
1F9DF 200D 2640 FE0F ; fully-qualified # 🧟♀️ E5.0 woman zombie
|
||||
1F9DF 200D 2640 ; minimally-qualified # 🧟♀ E5.0 woman zombie
|
||||
1F9CC ; fully-qualified # 🧌 E14.0 troll
|
||||
|
||||
# subgroup: person-activity
|
||||
1F486 ; fully-qualified # 💆 E0.6 person getting massage
|
||||
|
@ -3168,8 +3262,8 @@
|
|||
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
|
||||
1F463 ; fully-qualified # 👣 E0.6 footprints
|
||||
|
||||
# People & Body subtotal: 2899
|
||||
# People & Body subtotal: 494 w/o modifiers
|
||||
# People & Body subtotal: 2986
|
||||
# People & Body subtotal: 506 w/o modifiers
|
||||
|
||||
# group: Component
|
||||
|
||||
|
@ -3304,6 +3398,7 @@
|
|||
1F988 ; fully-qualified # 🦈 E3.0 shark
|
||||
1F419 ; fully-qualified # 🐙 E0.6 octopus
|
||||
1F41A ; fully-qualified # 🐚 E0.6 spiral shell
|
||||
1FAB8 ; fully-qualified # 🪸 E14.0 coral
|
||||
|
||||
# subgroup: animal-bug
|
||||
1F40C ; fully-qualified # 🐌 E0.6 snail
|
||||
|
@ -3329,6 +3424,7 @@
|
|||
1F490 ; fully-qualified # 💐 E0.6 bouquet
|
||||
1F338 ; fully-qualified # 🌸 E0.6 cherry blossom
|
||||
1F4AE ; fully-qualified # 💮 E0.6 white flower
|
||||
1FAB7 ; fully-qualified # 🪷 E14.0 lotus
|
||||
1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette
|
||||
1F3F5 ; unqualified # 🏵 E0.7 rosette
|
||||
1F339 ; fully-qualified # 🌹 E0.6 rose
|
||||
|
@ -3353,9 +3449,11 @@
|
|||
1F341 ; fully-qualified # 🍁 E0.6 maple leaf
|
||||
1F342 ; fully-qualified # 🍂 E0.6 fallen leaf
|
||||
1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
|
||||
1FAB9 ; fully-qualified # 🪹 E14.0 empty nest
|
||||
1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
|
||||
|
||||
# Animals & Nature subtotal: 147
|
||||
# Animals & Nature subtotal: 147 w/o modifiers
|
||||
# Animals & Nature subtotal: 151
|
||||
# Animals & Nature subtotal: 151 w/o modifiers
|
||||
|
||||
# group: Food & Drink
|
||||
|
||||
|
@ -3396,6 +3494,7 @@
|
|||
1F9C5 ; fully-qualified # 🧅 E12.0 onion
|
||||
1F344 ; fully-qualified # 🍄 E0.6 mushroom
|
||||
1F95C ; fully-qualified # 🥜 E3.0 peanuts
|
||||
1FAD8 ; fully-qualified # 🫘 E14.0 beans
|
||||
1F330 ; fully-qualified # 🌰 E0.6 chestnut
|
||||
|
||||
# subgroup: food-prepared
|
||||
|
@ -3491,6 +3590,7 @@
|
|||
1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs
|
||||
1F942 ; fully-qualified # 🥂 E3.0 clinking glasses
|
||||
1F943 ; fully-qualified # 🥃 E3.0 tumbler glass
|
||||
1FAD7 ; fully-qualified # 🫗 E14.0 pouring liquid
|
||||
1F964 ; fully-qualified # 🥤 E5.0 cup with straw
|
||||
1F9CB ; fully-qualified # 🧋 E13.0 bubble tea
|
||||
1F9C3 ; fully-qualified # 🧃 E12.0 beverage box
|
||||
|
@ -3504,10 +3604,11 @@
|
|||
1F374 ; fully-qualified # 🍴 E0.6 fork and knife
|
||||
1F944 ; fully-qualified # 🥄 E3.0 spoon
|
||||
1F52A ; fully-qualified # 🔪 E0.6 kitchen knife
|
||||
1FAD9 ; fully-qualified # 🫙 E14.0 jar
|
||||
1F3FA ; fully-qualified # 🏺 E1.0 amphora
|
||||
|
||||
# Food & Drink subtotal: 131
|
||||
# Food & Drink subtotal: 131 w/o modifiers
|
||||
# Food & Drink subtotal: 134
|
||||
# Food & Drink subtotal: 134 w/o modifiers
|
||||
|
||||
# group: Travel & Places
|
||||
|
||||
|
@ -3597,6 +3698,7 @@
|
|||
2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs
|
||||
2668 ; unqualified # ♨ E0.6 hot springs
|
||||
1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse
|
||||
1F6DD ; fully-qualified # 🛝 E14.0 playground slide
|
||||
1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel
|
||||
1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster
|
||||
1F488 ; fully-qualified # 💈 E0.6 barber pole
|
||||
|
@ -3652,6 +3754,7 @@
|
|||
1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum
|
||||
1F6E2 ; unqualified # 🛢 E0.7 oil drum
|
||||
26FD ; fully-qualified # ⛽ E0.6 fuel pump
|
||||
1F6DE ; fully-qualified # 🛞 E14.0 wheel
|
||||
1F6A8 ; fully-qualified # 🚨 E0.6 police car light
|
||||
1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light
|
||||
1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light
|
||||
|
@ -3660,6 +3763,7 @@
|
|||
|
||||
# subgroup: transport-water
|
||||
2693 ; fully-qualified # ⚓ E0.6 anchor
|
||||
1F6DF ; fully-qualified # 🛟 E14.0 ring buoy
|
||||
26F5 ; fully-qualified # ⛵ E0.6 sailboat
|
||||
1F6F6 ; fully-qualified # 🛶 E3.0 canoe
|
||||
1F6A4 ; fully-qualified # 🚤 E0.6 speedboat
|
||||
|
@ -3797,8 +3901,8 @@
|
|||
1F4A7 ; fully-qualified # 💧 E0.6 droplet
|
||||
1F30A ; fully-qualified # 🌊 E0.6 water wave
|
||||
|
||||
# Travel & Places subtotal: 264
|
||||
# Travel & Places subtotal: 264 w/o modifiers
|
||||
# Travel & Places subtotal: 267
|
||||
# Travel & Places subtotal: 267 w/o modifiers
|
||||
|
||||
# group: Activities
|
||||
|
||||
|
@ -3874,6 +3978,7 @@
|
|||
1F52E ; fully-qualified # 🔮 E0.6 crystal ball
|
||||
1FA84 ; fully-qualified # 🪄 E13.0 magic wand
|
||||
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
|
||||
1FAAC ; fully-qualified # 🪬 E14.0 hamsa
|
||||
1F3AE ; fully-qualified # 🎮 E0.6 video game
|
||||
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
|
||||
1F579 ; unqualified # 🕹 E0.7 joystick
|
||||
|
@ -3882,6 +3987,7 @@
|
|||
1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece
|
||||
1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear
|
||||
1FA85 ; fully-qualified # 🪅 E13.0 piñata
|
||||
1FAA9 ; fully-qualified # 🪩 E14.0 mirror ball
|
||||
1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls
|
||||
2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit
|
||||
2660 ; unqualified # ♠ E0.6 spade suit
|
||||
|
@ -3907,8 +4013,8 @@
|
|||
1F9F6 ; fully-qualified # 🧶 E11.0 yarn
|
||||
1FAA2 ; fully-qualified # 🪢 E13.0 knot
|
||||
|
||||
# Activities subtotal: 95
|
||||
# Activities subtotal: 95 w/o modifiers
|
||||
# Activities subtotal: 97
|
||||
# Activities subtotal: 97 w/o modifiers
|
||||
|
||||
# group: Objects
|
||||
|
||||
|
@ -4009,6 +4115,7 @@
|
|||
|
||||
# subgroup: computer
|
||||
1F50B ; fully-qualified # 🔋 E0.6 battery
|
||||
1FAAB ; fully-qualified # 🪫 E14.0 low battery
|
||||
1F50C ; fully-qualified # 🔌 E0.6 electric plug
|
||||
1F4BB ; fully-qualified # 💻 E0.6 laptop
|
||||
1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer
|
||||
|
@ -4207,7 +4314,9 @@
|
|||
1FA78 ; fully-qualified # 🩸 E12.0 drop of blood
|
||||
1F48A ; fully-qualified # 💊 E0.6 pill
|
||||
1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage
|
||||
1FA7C ; fully-qualified # 🩼 E14.0 crutch
|
||||
1FA7A ; fully-qualified # 🩺 E12.0 stethoscope
|
||||
1FA7B ; fully-qualified # 🩻 E14.0 x-ray
|
||||
|
||||
# subgroup: household
|
||||
1F6AA ; fully-qualified # 🚪 E0.6 door
|
||||
|
@ -4232,6 +4341,7 @@
|
|||
1F9FB ; fully-qualified # 🧻 E11.0 roll of paper
|
||||
1FAA3 ; fully-qualified # 🪣 E13.0 bucket
|
||||
1F9FC ; fully-qualified # 🧼 E11.0 soap
|
||||
1FAE7 ; fully-qualified # 🫧 E14.0 bubbles
|
||||
1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush
|
||||
1F9FD ; fully-qualified # 🧽 E11.0 sponge
|
||||
1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher
|
||||
|
@ -4246,9 +4356,10 @@
|
|||
26B1 ; unqualified # ⚱ E1.0 funeral urn
|
||||
1F5FF ; fully-qualified # 🗿 E0.6 moai
|
||||
1FAA7 ; fully-qualified # 🪧 E13.0 placard
|
||||
1FAAA ; fully-qualified # 🪪 E14.0 identification card
|
||||
|
||||
# Objects subtotal: 299
|
||||
# Objects subtotal: 299 w/o modifiers
|
||||
# Objects subtotal: 304
|
||||
# Objects subtotal: 304 w/o modifiers
|
||||
|
||||
# group: Symbols
|
||||
|
||||
|
@ -4409,6 +4520,7 @@
|
|||
2795 ; fully-qualified # ➕ E0.6 plus
|
||||
2796 ; fully-qualified # ➖ E0.6 minus
|
||||
2797 ; fully-qualified # ➗ E0.6 divide
|
||||
1F7F0 ; fully-qualified # 🟰 E14.0 heavy equals sign
|
||||
267E FE0F ; fully-qualified # ♾️ E11.0 infinity
|
||||
267E ; unqualified # ♾ E11.0 infinity
|
||||
|
||||
|
@ -4581,8 +4693,8 @@
|
|||
1F533 ; fully-qualified # 🔳 E0.6 white square button
|
||||
1F532 ; fully-qualified # 🔲 E0.6 black square button
|
||||
|
||||
# Symbols subtotal: 301
|
||||
# Symbols subtotal: 301 w/o modifiers
|
||||
# Symbols subtotal: 302
|
||||
# Symbols subtotal: 302 w/o modifiers
|
||||
|
||||
# group: Flags
|
||||
|
||||
|
@ -4871,7 +4983,7 @@
|
|||
# Flags subtotal: 275 w/o modifiers
|
||||
|
||||
# Status Counts
|
||||
# fully-qualified : 3512
|
||||
# fully-qualified : 3624
|
||||
# minimally-qualified : 817
|
||||
# unqualified : 252
|
||||
# component : 9
|
||||
|
|
BIN
resources/icons/ui/refresh.png
Normal file
BIN
resources/icons/ui/refresh.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
16
resources/icons/ui/refresh.svg
Normal file
16
resources/icons/ui/refresh.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="-10 0 1792 1792"
|
||||
id="svg866"
|
||||
width="1792"
|
||||
height="1792"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs870" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m 1629,1056 q 0,5 -1,7 -64,268 -268,434.5 Q 1156,1664 882,1664 736,1664 599.5,1609 463,1554 356,1452 l -129,129 q -19,19 -45,19 -26,0 -45,-19 -19,-19 -19,-45 v -448 q 0,-26 19,-45 19,-19 45,-19 h 448 q 26,0 45,19 19,19 19,45 0,26 -19,45 l -137,137 q 71,66 161,102 90,36 187,36 134,0 250,-65 116,-65 186,-179 11,-17 53,-117 8,-23 30,-23 h 192 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 25,-800 v 448 q 0,26 -19,45 -19,19 -45,19 h -448 q -26,0 -45,-19 -19,-19 -19,-45 0,-26 19,-45 L 1235,521 Q 1087,384 886,384 q -134,0 -250,65 -116,65 -186,179 -11,17 -53,117 -8,23 -30,23 H 168 q -13,0 -22.5,-9.5 Q 136,749 136,736 v -7 Q 201,461 406,294.5 611,128 886,128 q 146,0 284,55.5 138,55.5 245,156.5 l 130,-129 q 19,-19 45,-19 26,0 45,19 19,19 19,45 z"
|
||||
id="path864" />
|
||||
</svg>
|
After Width: | Height: | Size: 1 KiB |
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3325
resources/langs/nheko_id.ts
Normal file
3325
resources/langs/nheko_id.ts
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@
|
|||
<component type="desktop">
|
||||
<id>nheko.desktop</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0-or-later and CC-BY</project_license>
|
||||
<project_license>GPL-3.0-or-later</project_license>
|
||||
<name>nheko</name>
|
||||
<summary>Desktop client for the Matrix protocol</summary>
|
||||
<description>
|
||||
|
|
|
@ -12,6 +12,7 @@ Rectangle {
|
|||
|
||||
property string url
|
||||
property string userid
|
||||
property string roomid
|
||||
property string displayName
|
||||
property alias textColor: label.color
|
||||
property bool crop: true
|
||||
|
@ -35,10 +36,29 @@ Rectangle {
|
|||
font.pixelSize: avatar.height / 2
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: img.status != Image.Ready
|
||||
visible: img.status != Image.Ready && !Settings.useIdenticon
|
||||
color: Nheko.colors.text
|
||||
}
|
||||
|
||||
Image {
|
||||
id: identicon
|
||||
|
||||
anchors.fill: parent
|
||||
visible: Settings.useIdenticon && img.status != Image.Ready
|
||||
source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : ""
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
Ripple {
|
||||
rippleTarget: parent
|
||||
color: Qt.rgba(Nheko.colors.alternateBase.r, Nheko.colors.alternateBase.g, Nheko.colors.alternateBase.b, 0.5)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Image {
|
||||
id: img
|
||||
|
||||
|
@ -49,7 +69,7 @@ Rectangle {
|
|||
smooth: true
|
||||
sourceSize.width: avatar.width
|
||||
sourceSize.height: avatar.height
|
||||
source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100.0 : 25.0) + ((avatar.crop) ? "" : "&scale")) : ""
|
||||
source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale")) : ""
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import "components"
|
||||
import im.nheko 1.0
|
||||
|
||||
// this needs to be last
|
||||
import QtQml 2.15
|
||||
|
||||
Rectangle {
|
||||
id: chatPage
|
||||
|
||||
|
@ -18,7 +21,7 @@ Rectangle {
|
|||
|
||||
anchors.fill: parent
|
||||
singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width
|
||||
pageIndex: Rooms.currentRoom ? 2 : 1
|
||||
pageIndex: (Rooms.currentRoom || Rooms.currentRoomPreview.roomid) ? 2 : 1
|
||||
|
||||
AdaptiveLayoutElement {
|
||||
id: communityListC
|
||||
|
@ -41,6 +44,7 @@ Rectangle {
|
|||
value: communityListC.preferredWidth
|
||||
when: !adaptiveView.singlePageMode
|
||||
delayed: true
|
||||
restoreMode: Binding.RestoreBindingOrValue
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -66,6 +70,7 @@ Rectangle {
|
|||
value: roomListC.preferredWidth
|
||||
when: !adaptiveView.singlePageMode
|
||||
delayed: true
|
||||
restoreMode: Binding.RestoreBindingOrValue
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,29 +47,32 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: ItemDelegate {
|
||||
id: communityItem
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
property color backgroundColor: Nheko.colors.window
|
||||
property color importantText: Nheko.colors.text
|
||||
property color unimportantText: Nheko.colors.buttonText
|
||||
property color bubbleBackground: Nheko.colors.highlight
|
||||
property color bubbleText: Nheko.colors.highlightedText
|
||||
|
||||
color: background
|
||||
background: Rectangle {
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
width: ListView.view.width
|
||||
state: "normal"
|
||||
ToolTip.visible: hovered.hovered && collapsed
|
||||
ToolTip.visible: hovered && collapsed
|
||||
ToolTip.text: model.tooltip
|
||||
states: [
|
||||
State {
|
||||
name: "highlight"
|
||||
when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id)
|
||||
when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId == model.id)
|
||||
|
||||
PropertyChanges {
|
||||
target: communityItem
|
||||
background: Nheko.colors.dark
|
||||
backgroundColor: Nheko.colors.dark
|
||||
importantText: Nheko.colors.brightText
|
||||
unimportantText: Nheko.colors.brightText
|
||||
bubbleBackground: Nheko.colors.highlight
|
||||
|
@ -83,7 +86,7 @@ Page {
|
|||
|
||||
PropertyChanges {
|
||||
target: communityItem
|
||||
background: Nheko.colors.highlight
|
||||
backgroundColor: Nheko.colors.highlight
|
||||
importantText: Nheko.colors.highlightedText
|
||||
unimportantText: Nheko.colors.highlightedText
|
||||
bubbleBackground: Nheko.colors.highlightedText
|
||||
|
@ -93,24 +96,20 @@ Page {
|
|||
}
|
||||
]
|
||||
|
||||
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 {
|
||||
|
|
|
@ -139,6 +139,7 @@ Popup {
|
|||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.displayName
|
||||
userid: model.userid
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
||||
}
|
||||
|
@ -194,6 +195,7 @@ Popup {
|
|||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.roomName
|
||||
roomid: model.roomid
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: {
|
||||
popup.completionClicked(completer.completionAt(model.index));
|
||||
|
@ -225,6 +227,7 @@ Popup {
|
|||
height: popup.avatarHeight
|
||||
width: popup.avatarWidth
|
||||
displayName: model.roomName
|
||||
roomid: model.roomid
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
onClicked: popup.completionClicked(completer.completionAt(model.index))
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ Image {
|
|||
case Crypto.TOFU:
|
||||
return qsTr("Encrypted by an unverified device, but you have trusted that user so far.");
|
||||
default:
|
||||
return qsTr("Encrypted by an unverified device");
|
||||
return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -80,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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -23,6 +23,8 @@ ScrollView {
|
|||
|
||||
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
|
||||
|
||||
displayMarginBeginning: height / 2
|
||||
displayMarginEnd: height / 2
|
||||
model: room
|
||||
// reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
|
||||
//onModelChanged: if (room) room.sendReset()
|
||||
|
@ -33,7 +35,7 @@ ScrollView {
|
|||
verticalLayoutDirection: ListView.BottomToTop
|
||||
onCountChanged: {
|
||||
// Mark timeline as read
|
||||
if (atYEnd)
|
||||
if (atYEnd && room)
|
||||
model.currentIndex = 0;
|
||||
|
||||
}
|
||||
|
@ -233,8 +235,8 @@ ScrollView {
|
|||
id: dateBubble
|
||||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
visible: previousMessageDay !== day
|
||||
text: chat.model.formatDateSeparator(timestamp)
|
||||
visible: room && previousMessageDay !== day
|
||||
text: room ? room.formatDateSeparator(timestamp) : ""
|
||||
color: Nheko.colors.text
|
||||
height: Math.round(fontMetrics.height * 1.4)
|
||||
width: contentWidth * 1.2
|
||||
|
@ -257,10 +259,10 @@ ScrollView {
|
|||
|
||||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
url: chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
|
||||
url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
|
||||
displayName: userName
|
||||
userid: userId
|
||||
onClicked: chat.model.openUserProfile(userId)
|
||||
onClicked: room.openUserProfile(userId)
|
||||
ToolTip.visible: avatarHover.hovered
|
||||
ToolTip.text: userid
|
||||
|
||||
|
@ -276,7 +278,7 @@ ScrollView {
|
|||
}
|
||||
|
||||
function onScrollToIndex(index) {
|
||||
chat.positionViewAtIndex(index, ListView.Visible);
|
||||
chat.positionViewAtIndex(index, ListView.Center);
|
||||
}
|
||||
|
||||
target: chat.model
|
||||
|
@ -361,7 +363,7 @@ ScrollView {
|
|||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
width: chat.delegateMaxWidth
|
||||
height: section ? section.height + timelinerow.height : timelinerow.height
|
||||
height: Math.max(section.active ? section.height + timelinerow.height : timelinerow.height, 10)
|
||||
|
||||
Rectangle {
|
||||
id: scrollHighlight
|
||||
|
@ -420,6 +422,7 @@ ScrollView {
|
|||
property string userName: wrapper.userName
|
||||
property var timestamp: wrapper.timestamp
|
||||
|
||||
z: 4
|
||||
active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day
|
||||
//asynchronous: true
|
||||
sourceComponent: sectionHeader
|
||||
|
@ -648,4 +651,39 @@ ScrollView {
|
|||
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: replyContextMenu
|
||||
|
||||
property string text
|
||||
property string link
|
||||
|
||||
function show(text_, link_) {
|
||||
text = text_;
|
||||
link = link_;
|
||||
open();
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: replyContextMenu.text
|
||||
enabled: visible
|
||||
text: qsTr("&Copy")
|
||||
onTriggered: Clipboard.text = replyContextMenu.text
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: replyContextMenu.link
|
||||
enabled: visible
|
||||
text: qsTr("Copy &link location")
|
||||
onTriggered: Clipboard.text = replyContextMenu.link
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
visible: true
|
||||
enabled: visible
|
||||
text: qsTr("&Go to quoted message")
|
||||
onTriggered: chat.model.showEvent(eventId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
38
resources/qml/NotificationWarning.qml
Normal file
38
resources/qml/NotificationWarning.qml
Normal file
|
@ -0,0 +1,38 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
implicitHeight: warningRect.visible ? warningDisplay.implicitHeight : 0
|
||||
height: implicitHeight
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
id: warningRect
|
||||
|
||||
visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
|
||||
color: Nheko.colors.base
|
||||
anchors.fill: parent
|
||||
z: 3
|
||||
|
||||
Label {
|
||||
id: warningDisplay
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.bottom: parent.bottom
|
||||
color: Nheko.theme.red
|
||||
text: qsTr("You are about to notify the whole room")
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -39,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;
|
||||
|
|
|
@ -16,6 +16,14 @@ Page {
|
|||
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
|
||||
property bool collapsed: false
|
||||
|
||||
Component {
|
||||
id: roomDirectoryComponent
|
||||
|
||||
RoomDirectory {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: roomlist
|
||||
|
||||
|
@ -63,19 +71,9 @@ Page {
|
|||
}
|
||||
}
|
||||
|
||||
Platform.MessageDialog {
|
||||
id: leaveRoomDialog
|
||||
|
||||
title: qsTr("Leave Room")
|
||||
text: qsTr("Are you sure you want to leave this room?")
|
||||
modality: Qt.ApplicationModal
|
||||
onAccepted: Rooms.leave(roomContextMenu.roomid)
|
||||
buttons: Dialog.Ok | Dialog.Cancel
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Leave room")
|
||||
onTriggered: leaveRoomDialog.open()
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {
|
||||
|
@ -116,10 +114,10 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: ItemDelegate {
|
||||
id: roomItem
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
property color backgroundColor: Nheko.colors.window
|
||||
property color importantText: Nheko.colors.text
|
||||
property color unimportantText: Nheko.colors.buttonText
|
||||
property color bubbleBackground: Nheko.colors.highlight
|
||||
|
@ -135,21 +133,34 @@ Page {
|
|||
required property int notificationCount
|
||||
required property bool hasLoudNotification
|
||||
required property bool hasUnreadMessages
|
||||
required property bool isDirect
|
||||
required property string directChatOtherUserId
|
||||
|
||||
color: background
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
width: ListView.view.width
|
||||
state: "normal"
|
||||
ToolTip.visible: hovered.hovered && collapsed
|
||||
ToolTip.visible: hovered && collapsed
|
||||
ToolTip.text: roomName
|
||||
onClicked: {
|
||||
console.log("tapped " + roomId);
|
||||
if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
|
||||
Rooms.setCurrentRoom(roomId);
|
||||
else
|
||||
Rooms.resetCurrentRoom();
|
||||
}
|
||||
onPressAndHold: {
|
||||
if (!isInvite)
|
||||
roomContextMenu.show(roomId, tags);
|
||||
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: "highlight"
|
||||
when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
|
||||
when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
|
||||
|
||||
PropertyChanges {
|
||||
target: roomItem
|
||||
background: Nheko.colors.dark
|
||||
backgroundColor: Nheko.colors.dark
|
||||
importantText: Nheko.colors.brightText
|
||||
unimportantText: Nheko.colors.brightText
|
||||
bubbleBackground: Nheko.colors.highlight
|
||||
|
@ -163,7 +174,7 @@ Page {
|
|||
|
||||
PropertyChanges {
|
||||
target: roomItem
|
||||
background: Nheko.colors.highlight
|
||||
backgroundColor: Nheko.colors.highlight
|
||||
importantText: Nheko.colors.highlightedText
|
||||
unimportantText: Nheko.colors.highlightedText
|
||||
bubbleBackground: Nheko.colors.highlightedText
|
||||
|
@ -189,27 +200,6 @@ Page {
|
|||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
margin: -Nheko.paddingSmall
|
||||
onSingleTapped: {
|
||||
if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
|
||||
Rooms.setCurrentRoom(roomId);
|
||||
else
|
||||
Rooms.resetCurrentRoom();
|
||||
}
|
||||
onLongPressed: {
|
||||
if (!isInvite)
|
||||
roomContextMenu.show(roomId, tags);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hovered
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
@ -229,6 +219,8 @@ Page {
|
|||
width: avatarSize
|
||||
url: avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: roomName
|
||||
userid: isDirect ? directChatOtherUserId : ""
|
||||
roomid: roomId
|
||||
|
||||
Rectangle {
|
||||
id: collapsedNotificationBubble
|
||||
|
@ -359,6 +351,10 @@ Page {
|
|||
visible: hasUnreadMessages
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: backgroundColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -500,6 +496,91 @@ Page {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: unverifiedStuffBubble
|
||||
|
||||
color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1)
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: explanation.height + Nheko.paddingMedium * 2
|
||||
visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified
|
||||
|
||||
RowLayout {
|
||||
id: unverifiedStuffBubbleContainer
|
||||
|
||||
width: parent.width
|
||||
height: explanation.height + Nheko.paddingMedium * 2
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
id: explanation
|
||||
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.rightMargin: Nheko.paddingSmall
|
||||
color: Nheko.colors.buttonText
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
switch (SelfVerificationStatus.status) {
|
||||
case SelfVerificationStatus.NoMasterKey:
|
||||
//: Cross-signing setup has not run yet.
|
||||
return qsTr("Encryption not set up");
|
||||
case SelfVerificationStatus.UnverifiedMasterKey:
|
||||
//: The user just signed in with this device and hasn't verified their master key.
|
||||
return qsTr("Unverified login");
|
||||
case SelfVerificationStatus.UnverifiedDevices:
|
||||
//: There are unverified devices signed in to this account.
|
||||
return qsTr("Please verify your other devices");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
id: closeUnverifiedBubble
|
||||
|
||||
Layout.rightMargin: Nheko.paddingMedium
|
||||
Layout.topMargin: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
hoverEnabled: true
|
||||
width: fontMetrics.font.pixelSize
|
||||
height: fontMetrics.font.pixelSize
|
||||
image: ":/icons/icons/ui/remove-symbol.png"
|
||||
ToolTip.visible: closeUnverifiedBubble.hovered
|
||||
ToolTip.text: qsTr("Close")
|
||||
onClicked: unverifiedStuffBubble.visible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: verifyButtonHovered
|
||||
|
||||
enabled: !closeUnverifiedBubble.hovered
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: !closeUnverifiedBubble.hovered
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onSingleTapped: {
|
||||
if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
|
||||
SelfVerificationStatus.verifyUnverifiedDevices();
|
||||
else
|
||||
SelfVerificationStatus.statusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Nheko.theme.separator
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
visible: unverifiedStuffBubble.visible
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
|
@ -563,6 +644,10 @@ Page {
|
|||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Room directory")
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
onClicked: {
|
||||
var win = roomDirectoryComponent.createObject(timelineRoot);
|
||||
win.show();
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
|
|
|
@ -111,6 +111,30 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: logoutDialog
|
||||
|
||||
LogoutDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: joinRoomDialog
|
||||
|
||||
JoinRoomDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: leaveRoomComponent
|
||||
|
||||
LeaveRoomDialog {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: {
|
||||
|
@ -120,6 +144,11 @@ Page {
|
|||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+A"
|
||||
onActivated: Rooms.nextRoomWithActivity()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+Down"
|
||||
onActivated: Rooms.nextRoom()
|
||||
|
@ -130,6 +159,20 @@ Page {
|
|||
onActivated: Rooms.previousRoom()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onOpenLogoutDialog() {
|
||||
var dialog = logoutDialog.createObject(timelineRoot);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
function onOpenJoinRoomDialog() {
|
||||
var dialog = joinRoomDialog.createObject(timelineRoot);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
target: Nheko
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNewDeviceVerificationRequest(flow) {
|
||||
var dialog = deviceVerificationDialog.createObject(timelineRoot, {
|
||||
|
@ -138,6 +181,10 @@ Page {
|
|||
dialog.show();
|
||||
}
|
||||
|
||||
target: VerificationManager
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onOpenProfile(profile) {
|
||||
var userProfile = userProfileComponent.createObject(timelineRoot, {
|
||||
"profile": profile
|
||||
|
@ -176,6 +223,13 @@ Page {
|
|||
dialog.show();
|
||||
}
|
||||
|
||||
function onOpenLeaveRoomDialog(roomid) {
|
||||
var dialog = leaveRoomComponent.createObject(timelineRoot, {
|
||||
"roomId": roomid
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
target: TimelineManager
|
||||
}
|
||||
|
||||
|
@ -190,6 +244,94 @@ Page {
|
|||
target: CallManager
|
||||
}
|
||||
|
||||
SelfVerificationCheck {
|
||||
}
|
||||
|
||||
InputDialog {
|
||||
id: uiaPassPrompt
|
||||
|
||||
echoMode: TextInput.Password
|
||||
title: UIA.title
|
||||
prompt: qsTr("Please enter your login password to continue:")
|
||||
onAccepted: (t) => {
|
||||
return UIA.continuePassword(t);
|
||||
}
|
||||
}
|
||||
|
||||
InputDialog {
|
||||
id: uiaEmailPrompt
|
||||
|
||||
title: UIA.title
|
||||
prompt: qsTr("Please enter a valid email address to continue:")
|
||||
onAccepted: (t) => {
|
||||
return UIA.continueEmail(t);
|
||||
}
|
||||
}
|
||||
|
||||
PhoneNumberInputDialog {
|
||||
id: uiaPhoneNumberPrompt
|
||||
|
||||
title: UIA.title
|
||||
prompt: qsTr("Please enter a valid phone number to continue:")
|
||||
onAccepted: (p, t) => {
|
||||
return UIA.continuePhoneNumber(p, t);
|
||||
}
|
||||
}
|
||||
|
||||
InputDialog {
|
||||
id: uiaTokenPrompt
|
||||
|
||||
title: UIA.title
|
||||
prompt: qsTr("Please enter the token, which has been sent to you:")
|
||||
onAccepted: (t) => {
|
||||
return UIA.submit3pidToken(t);
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MessageDialog {
|
||||
id: uiaErrorDialog
|
||||
|
||||
buttons: Platform.MessageDialog.Ok
|
||||
}
|
||||
|
||||
Platform.MessageDialog {
|
||||
id: uiaConfirmationLinkDialog
|
||||
|
||||
buttons: Platform.MessageDialog.Ok
|
||||
text: qsTr("Wait for the confirmation link to arrive, then continue.")
|
||||
onAccepted: UIA.continue3pidReceived()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onPassword() {
|
||||
console.log("UIA: password needed");
|
||||
uiaPassPrompt.show();
|
||||
}
|
||||
|
||||
function onEmail() {
|
||||
uiaEmailPrompt.show();
|
||||
}
|
||||
|
||||
function onPhoneNumber() {
|
||||
uiaPhoneNumberPrompt.show();
|
||||
}
|
||||
|
||||
function onPrompt3pidToken() {
|
||||
uiaTokenPrompt.show();
|
||||
}
|
||||
|
||||
function onConfirm3pidToken() {
|
||||
uiaConfirmationLinkDialog.open();
|
||||
}
|
||||
|
||||
function onError(msg) {
|
||||
uiaErrorDialog.text = msg;
|
||||
uiaErrorDialog.open();
|
||||
}
|
||||
|
||||
target: UIA
|
||||
}
|
||||
|
||||
ChatPage {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
|
305
resources/qml/SelfVerificationCheck.qml
Normal file
305
resources/qml/SelfVerificationCheck.qml
Normal file
|
@ -0,0 +1,305 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./components/"
|
||||
import Qt.labs.platform 1.1 as P
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import im.nheko 1.0
|
||||
|
||||
Item {
|
||||
visible: false
|
||||
enabled: false
|
||||
|
||||
Dialog {
|
||||
id: showRecoverKeyDialog
|
||||
|
||||
property string recoveryKey: ""
|
||||
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
height: content.height + implicitFooterHeight + implicitHeaderHeight
|
||||
width: content.width
|
||||
padding: 0
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
closePolicy: Popup.NoAutoClose
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Do not pass go! Do not collect $200!")
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: TextEdit.AlignHCenter
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
text: showRecoverKeyDialog.recoveryKey
|
||||
color: Nheko.colors.text
|
||||
font.bold: true
|
||||
wrapMode: TextEdit.Wrap
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Nheko.colors.window
|
||||
border.color: Nheko.theme.separator
|
||||
border.width: 1
|
||||
radius: Nheko.paddingSmall
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
P.MessageDialog {
|
||||
id: successDialog
|
||||
|
||||
buttons: P.MessageDialog.Ok
|
||||
text: qsTr("Encryption setup successfully")
|
||||
}
|
||||
|
||||
P.MessageDialog {
|
||||
id: failureDialog
|
||||
|
||||
property string errorMessage
|
||||
|
||||
buttons: P.MessageDialog.Ok
|
||||
text: qsTr("Failed to setup encryption: %1").arg(errorMessage)
|
||||
}
|
||||
|
||||
MainWindowDialog {
|
||||
id: bootstrapCrosssigning
|
||||
|
||||
onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked)
|
||||
|
||||
GridLayout {
|
||||
id: grid
|
||||
|
||||
width: bootstrapCrosssigning.useableWidth
|
||||
columns: 2
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
z: 1
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.columnSpan: 2
|
||||
font.pointSize: fontMetrics.font.pointSize * 2
|
||||
text: qsTr("Setup Encryption")
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.columnSpan: 2
|
||||
Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
|
||||
text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!")
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.columnSpan: 1
|
||||
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
|
||||
text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!"
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.preferredHeight: storeSecretsOnline.height
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
ToggleButton {
|
||||
id: storeSecretsOnline
|
||||
|
||||
checked: true
|
||||
onClicked: console.log("Store secrets toggled: " + checked)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.columnSpan: 1
|
||||
Layout.rowSpan: 2
|
||||
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
|
||||
visible: storeSecretsOnline.checked
|
||||
text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)"
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.topMargin: Nheko.paddingLarge
|
||||
Layout.preferredHeight: storeSecretsOnline.height
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.rowSpan: usePassword.checked ? 1 : 2
|
||||
Layout.fillWidth: true
|
||||
visible: storeSecretsOnline.checked
|
||||
|
||||
ToggleButton {
|
||||
id: usePassword
|
||||
|
||||
checked: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
id: passwordField
|
||||
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.columnSpan: 1
|
||||
Layout.fillWidth: true
|
||||
visible: storeSecretsOnline.checked && usePassword.checked
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.columnSpan: 1
|
||||
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
|
||||
text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages."
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.preferredHeight: storeSecretsOnline.height
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
ToggleButton {
|
||||
id: useOnlineKeyBackup
|
||||
|
||||
checked: true
|
||||
onClicked: console.log("Online key backup toggled: " + checked)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Nheko.colors.window
|
||||
border.color: Nheko.theme.separator
|
||||
border.width: 1
|
||||
radius: Nheko.paddingSmall
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MainWindowDialog {
|
||||
id: verifyMasterKey
|
||||
|
||||
standardButtons: Dialog.Cancel
|
||||
|
||||
GridLayout {
|
||||
id: masterGrid
|
||||
|
||||
width: verifyMasterKey.useableWidth
|
||||
columns: 1
|
||||
z: 1
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
//Layout.columnSpan: 2
|
||||
font.pointSize: fontMetrics.font.pointSize * 2
|
||||
text: qsTr("Activate Encryption")
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.margins: Nheko.paddingMedium
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
//Layout.columnSpan: 2
|
||||
Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2
|
||||
text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.")
|
||||
color: Nheko.colors.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
FlatButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("verify")
|
||||
onClicked: {
|
||||
SelfVerificationStatus.verifyMasterKey();
|
||||
verifyMasterKey.close();
|
||||
}
|
||||
}
|
||||
|
||||
FlatButton {
|
||||
visible: SelfVerificationStatus.hasSSSS
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("enter passphrase")
|
||||
onClicked: {
|
||||
SelfVerificationStatus.verifyMasterKeyWithPassphrase();
|
||||
verifyMasterKey.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onStatusChanged() {
|
||||
console.log("STATUS CHANGED: " + SelfVerificationStatus.status);
|
||||
if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) {
|
||||
bootstrapCrosssigning.open();
|
||||
} else if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedMasterKey) {
|
||||
verifyMasterKey.open();
|
||||
} else {
|
||||
bootstrapCrosssigning.close();
|
||||
verifyMasterKey.close();
|
||||
}
|
||||
}
|
||||
|
||||
function onShowRecoveryKey(key) {
|
||||
showRecoverKeyDialog.recoveryKey = key;
|
||||
showRecoverKeyDialog.open();
|
||||
}
|
||||
|
||||
function onSetupCompleted() {
|
||||
successDialog.open();
|
||||
}
|
||||
|
||||
function onSetupFailed(m) {
|
||||
failureDialog.errorMessage = m;
|
||||
failureDialog.open();
|
||||
}
|
||||
|
||||
target: SelfVerificationStatus
|
||||
}
|
||||
|
||||
}
|
|
@ -24,7 +24,7 @@ Item {
|
|||
property bool showBackButton: false
|
||||
|
||||
Label {
|
||||
visible: !room && !TimelineManager.isInitialSync && !roomPreview
|
||||
visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No room open")
|
||||
font.pointSize: 24
|
||||
|
@ -84,14 +84,9 @@ Item {
|
|||
target: timelineView
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: room || roomPreview
|
||||
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
|
||||
|
|
|
@ -13,10 +13,13 @@ Rectangle {
|
|||
|
||||
property bool showBackButton: false
|
||||
property string roomName: room ? room.roomName : qsTr("No room selected")
|
||||
property string roomId: room ? room.roomId : ""
|
||||
property string avatarUrl: room ? room.roomAvatarUrl : ""
|
||||
property string roomTopic: room ? room.roomTopic : ""
|
||||
property bool isEncrypted: room ? room.isEncrypted : false
|
||||
property int trustlevel: room ? room.trustlevel : Crypto.Unverified
|
||||
property bool isDirect: room ? room.isDirect : false
|
||||
property string directChatOtherUserId: room ? room.directChatOtherUserId : ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: topLayout.height + Nheko.paddingMedium * 2
|
||||
|
@ -65,10 +68,12 @@ Rectangle {
|
|||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
url: avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
roomid: roomId
|
||||
userid: isDirect ? directChatOtherUserId : ""
|
||||
displayName: roomName
|
||||
onClicked: {
|
||||
if (room)
|
||||
TimelineManager.openRoomSettings(room.roomId);
|
||||
TimelineManager.openRoomSettings(roomId);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +114,7 @@ Rectangle {
|
|||
case Crypto.Verified:
|
||||
return qsTr("This room contains only verified devices.");
|
||||
case Crypto.TOFU:
|
||||
return qsTr("This rooms contain verified devices and devices which have never changed their master key.");
|
||||
return qsTr("This room contains verified devices and devices which have never changed their master key.");
|
||||
default:
|
||||
return qsTr("This room contains unverified devices!");
|
||||
}
|
||||
|
@ -135,7 +140,7 @@ Rectangle {
|
|||
Platform.MenuItem {
|
||||
visible: room ? room.permissions.canInvite() : false
|
||||
text: qsTr("Invite users")
|
||||
onTriggered: TimelineManager.openInviteUsers(room.roomId)
|
||||
onTriggered: TimelineManager.openInviteUsers(roomId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
|
@ -145,12 +150,12 @@ Rectangle {
|
|||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Leave room")
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId)
|
||||
onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onTriggered: TimelineManager.openRoomSettings(room.roomId)
|
||||
onTriggered: TimelineManager.openRoomSettings(roomId)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,270 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./device-verification"
|
||||
import "./ui"
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
// this does not work in ApplicationWindow, just in Window
|
||||
//transientParent: Nheko.mainwindow()
|
||||
|
||||
id: userProfileDialog
|
||||
|
||||
property var profile
|
||||
|
||||
height: 650
|
||||
width: 420
|
||||
minimumHeight: 420
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
Component.onCompleted: Nheko.reparent(userProfileDialog)
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: userProfileDialog.close()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentL
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Avatar {
|
||||
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
height: 130
|
||||
width: 130
|
||||
displayName: profile.displayName
|
||||
userid: profile.userid
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: profile.isSelf ? profile.changeAvatar() : TimelineManager.openImageOverlay(profile.avatarUrl, "")
|
||||
}
|
||||
|
||||
Spinner {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
running: profile.isLoading
|
||||
visible: profile.isLoading
|
||||
foreground: Nheko.colors.mid
|
||||
}
|
||||
|
||||
Text {
|
||||
id: errorText
|
||||
|
||||
color: "red"
|
||||
visible: opacity > 0
|
||||
opacity: 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: hideErrorAnimation
|
||||
|
||||
running: false
|
||||
|
||||
PauseAnimation {
|
||||
duration: 4000
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: errorText
|
||||
property: 'opacity'
|
||||
to: 0
|
||||
duration: 1000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDisplayError(errorMessage) {
|
||||
errorText.text = errorMessage;
|
||||
errorText.opacity = 1;
|
||||
hideErrorAnimation.restart();
|
||||
}
|
||||
|
||||
target: profile
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: displayUsername
|
||||
|
||||
property bool isUsernameEditingAllowed
|
||||
|
||||
readOnly: !isUsernameEditingAllowed
|
||||
text: profile.displayName
|
||||
font.pixelSize: 20
|
||||
color: TimelineManager.userColor(profile.userid, Nheko.colors.window)
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
selectByMouse: true
|
||||
onAccepted: {
|
||||
profile.changeUsername(displayUsername.text);
|
||||
displayUsername.isUsernameEditingAllowed = false;
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
visible: profile.isSelf
|
||||
anchors.leftMargin: 5
|
||||
anchors.left: displayUsername.right
|
||||
anchors.verticalCenter: displayUsername.verticalCenter
|
||||
image: displayUsername.isUsernameEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
|
||||
onClicked: {
|
||||
if (displayUsername.isUsernameEditingAllowed) {
|
||||
profile.changeUsername(displayUsername.text);
|
||||
displayUsername.isUsernameEditingAllowed = false;
|
||||
} else {
|
||||
displayUsername.isUsernameEditingAllowed = true;
|
||||
displayUsername.focus = true;
|
||||
displayUsername.selectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: profile.userid
|
||||
font.pixelSize: 15
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
id: verifyUserButton
|
||||
|
||||
text: qsTr("Verify")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enabled: profile.userVerified != Crypto.Verified
|
||||
visible: profile.userVerified != Crypto.Verified && !profile.isSelf && profile.userVerificationEnabled
|
||||
onClicked: profile.verify()
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : Nheko.colors.buttonText)
|
||||
visible: profile.userVerified != Crypto.Unverified
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
// ImageButton{
|
||||
// image:":/icons/icons/ui/volume-off-indicator.png"
|
||||
// Layout.margins: {
|
||||
// left: 5
|
||||
// right: 5
|
||||
// }
|
||||
// ToolTip.visible: hovered
|
||||
// ToolTip.text: qsTr("Ignore messages from this user")
|
||||
// onClicked : {
|
||||
// profile.ignoreUser()
|
||||
// }
|
||||
// }
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 8
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/black-bubble-speech.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Start a private chat")
|
||||
onClicked: profile.startChat()
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/round-remove-button.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Kick the user")
|
||||
onClicked: profile.kickUser()
|
||||
visible: profile.room ? profile.room.permissions.canKick() : false
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Ban the user")
|
||||
onClicked: profile.banUser()
|
||||
visible: profile.room ? profile.room.permissions.canBan() : false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: devicelist
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 200
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
spacing: 8
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: profile.deviceList
|
||||
|
||||
delegate: RowLayout {
|
||||
width: devicelist.width
|
||||
spacing: 4
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
color: Nheko.colors.text
|
||||
text: model.deviceId
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignRight
|
||||
elide: Text.ElideRight
|
||||
color: Nheko.colors.text
|
||||
text: model.deviceName
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red"))
|
||||
}
|
||||
|
||||
Button {
|
||||
id: verifyButton
|
||||
|
||||
visible: (!profile.userVerificationEnabled && !profile.isSelf) || (profile.isSelf && (model.verificationStatus != VerificationStatus.VERIFIED || !profile.userVerificationEnabled))
|
||||
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
|
||||
onClicked: {
|
||||
if (model.verificationStatus == VerificationStatus.VERIFIED)
|
||||
profile.unverify(model.deviceId);
|
||||
else
|
||||
profile.verify(model.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
standardButtons: DialogButtonBox.Ok
|
||||
onAccepted: userProfileDialog.close()
|
||||
}
|
||||
|
||||
}
|
|
@ -23,6 +23,8 @@ Rectangle {
|
|||
required property int index
|
||||
required property int selectedIndex
|
||||
property bool crop: true
|
||||
property alias roomid: avatar.roomid
|
||||
property alias userid: avatar.userid
|
||||
|
||||
color: background
|
||||
height: avatarSize + 2 * Nheko.paddingMedium
|
||||
|
|
41
resources/qml/components/MainWindowDialog.qml
Normal file
41
resources/qml/components/MainWindowDialog.qml
Normal file
|
@ -0,0 +1,41 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1 as P
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import im.nheko 1.0
|
||||
|
||||
Dialog {
|
||||
default property alias inner: scroll.data
|
||||
property int useableWidth: scroll.width - scroll.ScrollBar.vertical.width
|
||||
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
height: (Math.floor(parent.height / 2) - Nheko.paddingLarge) * 2
|
||||
width: (Math.floor(parent.width / 2) - Nheko.paddingLarge) * 2
|
||||
padding: 0
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
closePolicy: Popup.NoAutoClose
|
||||
contentChildren: [
|
||||
ScrollView {
|
||||
id: scroll
|
||||
|
||||
clip: true
|
||||
anchors.fill: parent
|
||||
ScrollBar.horizontal.visible: false
|
||||
ScrollBar.vertical.visible: true
|
||||
}
|
||||
]
|
||||
|
||||
background: Rectangle {
|
||||
color: Nheko.colors.window
|
||||
border.color: Nheko.theme.separator
|
||||
border.width: 1
|
||||
radius: Nheko.paddingSmall
|
||||
}
|
||||
|
||||
}
|
|
@ -3,11 +3,11 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import im.nheko 1.0
|
||||
|
||||
ColumnLayout {
|
||||
Column {
|
||||
id: r
|
||||
|
||||
required property int encryptionError
|
||||
|
|
|
@ -14,6 +14,7 @@ Item {
|
|||
required property string body
|
||||
required property string filename
|
||||
required property bool isReply
|
||||
required property string eventId
|
||||
property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth)
|
||||
property double tempHeight: tempWidth * proportionalHeight
|
||||
property double divisor: isReply ? 5 : 3
|
||||
|
@ -37,6 +38,7 @@ Item {
|
|||
Image {
|
||||
id: img
|
||||
|
||||
visible: !mxcimage.loaded
|
||||
anchors.fill: parent
|
||||
source: url.replace("mxc://", "image://MxcImage/")
|
||||
asynchronous: true
|
||||
|
@ -53,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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
|
@ -36,11 +37,6 @@ Item {
|
|||
width: parent.width
|
||||
height: replyContainer.height
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: chat.model.showEvent(eventId)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
CursorShape {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
@ -62,6 +58,19 @@ Item {
|
|||
anchors.leftMargin: 4
|
||||
width: parent.width - 8
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onSingleTapped: chat.model.showEvent(r.eventId)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onLongPressed: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
|
||||
onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight))
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
Text {
|
||||
id: userName_
|
||||
|
||||
|
@ -99,6 +108,7 @@ Item {
|
|||
callType: r.callType
|
||||
relatedEventCacheBuster: r.relatedEventCacheBuster
|
||||
encryptionError: r.encryptionError
|
||||
// This is disabled so that left clicking the reply goes to its location
|
||||
enabled: false
|
||||
width: parent.width
|
||||
isReply: true
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick.Controls 2.3
|
||||
import im.nheko 1.0
|
||||
|
||||
MatrixText {
|
||||
|
@ -28,6 +29,7 @@ MatrixText {
|
|||
border-collapse: collapse;
|
||||
border: 1px solid " + Nheko.colors.text + ";
|
||||
}
|
||||
blockquote { margin-left: 1em; }
|
||||
</style>
|
||||
" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>").replace("<del>", "<s>").replace("</del>", "</s>").replace("<strike>", "<s>").replace("</strike>", "</s>")
|
||||
width: parent ? parent.width : undefined
|
||||
|
@ -35,4 +37,11 @@ MatrixText {
|
|||
clip: isReply
|
||||
selectByMouse: !Settings.mobileMode && !isReply
|
||||
font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
|
||||
|
||||
CursorShape {
|
||||
enabled: isReply
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ ApplicationWindow {
|
|||
|
||||
property var flow
|
||||
|
||||
onClosing: TimelineManager.removeVerificationFlow(flow)
|
||||
onClosing: VerificationManager.removeVerificationFlow(flow)
|
||||
title: stack.currentItem.title
|
||||
modality: Qt.NonModal
|
||||
palette: Nheko.colors
|
||||
height: stack.implicitHeight
|
||||
width: stack.implicitWidth
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(dialog)
|
||||
|
||||
StackView {
|
||||
|
|
|
@ -33,9 +33,9 @@ Pane {
|
|||
case DeviceVerificationFlow.User:
|
||||
return qsTr("Other party canceled the verification.");
|
||||
case DeviceVerificationFlow.OutOfOrder:
|
||||
return qsTr("Device verification timed out.");
|
||||
return qsTr("Verification messages received out of order!");
|
||||
default:
|
||||
return "Unknown verification error.";
|
||||
return qsTr("Unknown verification error.");
|
||||
}
|
||||
}
|
||||
color: Nheko.colors.text
|
||||
|
|
|
@ -23,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 {
|
||||
|
|
|
@ -27,7 +27,7 @@ ApplicationWindow {
|
|||
palette: Nheko.colors
|
||||
color: Nheko.colors.base
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
|
||||
AdaptiveLayout {
|
||||
id: adaptiveView
|
||||
|
@ -61,6 +61,7 @@ ApplicationWindow {
|
|||
header: AvatarListTile {
|
||||
title: imagePack.packname
|
||||
avatarUrl: imagePack.avatarUrl
|
||||
roomid: imagePack.statekey
|
||||
subtitle: imagePack.statekey
|
||||
index: -1
|
||||
selectedIndex: currentImageIndex
|
||||
|
@ -90,7 +91,7 @@ ApplicationWindow {
|
|||
|
||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
fileMode: FileDialog.OpenFiles
|
||||
nameFilters: [qsTr("Stickers (*.png *.webp)")]
|
||||
nameFilters: [qsTr("Stickers (*.png *.webp *.gif *.jpg *.jpeg)")]
|
||||
onAccepted: imagePack.addStickers(files)
|
||||
}
|
||||
|
||||
|
@ -142,6 +143,7 @@ ApplicationWindow {
|
|||
Layout.columnSpan: 2
|
||||
url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: imagePack.packname
|
||||
roomid: imagePack.statekey
|
||||
height: 130
|
||||
width: 130
|
||||
crop: false
|
||||
|
@ -219,6 +221,7 @@ ApplicationWindow {
|
|||
Layout.columnSpan: 2
|
||||
url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/")
|
||||
displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
|
||||
roomid: displayName
|
||||
height: 130
|
||||
width: 130
|
||||
crop: false
|
||||
|
@ -265,6 +268,20 @@ ApplicationWindow {
|
|||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: qsTr("Remove from pack")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Remove")
|
||||
onClicked: {
|
||||
let temp = currentImageIndex;
|
||||
currentImageIndex = -1;
|
||||
imagePack.remove(temp);
|
||||
}
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillHeight: true
|
||||
|
|
|
@ -25,7 +25,7 @@ ApplicationWindow {
|
|||
palette: Nheko.colors
|
||||
color: Nheko.colors.base
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(win)
|
||||
|
||||
Component {
|
||||
|
@ -101,6 +101,7 @@ ApplicationWindow {
|
|||
required property string displayName
|
||||
required property bool fromAccountData
|
||||
required property bool fromCurrentRoom
|
||||
required property string statekey
|
||||
|
||||
title: displayName
|
||||
subtitle: {
|
||||
|
@ -112,6 +113,7 @@ ApplicationWindow {
|
|||
return qsTr("Globally enabled pack");
|
||||
}
|
||||
selectedIndex: currentPackIndex
|
||||
roomid: statekey
|
||||
|
||||
TapHandler {
|
||||
onSingleTapped: currentPackIndex = index
|
||||
|
@ -135,6 +137,7 @@ ApplicationWindow {
|
|||
property string packName: currentPack ? currentPack.packname : ""
|
||||
property string attribution: currentPack ? currentPack.attribution : ""
|
||||
property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
|
||||
property string statekey: currentPack ? currentPack.statekey : ""
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingLarge
|
||||
|
@ -143,6 +146,7 @@ ApplicationWindow {
|
|||
Avatar {
|
||||
url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: packinfo.packName
|
||||
roomid: packinfo.statekey
|
||||
height: 100
|
||||
width: 100
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
|
|
@ -12,6 +12,7 @@ ApplicationWindow {
|
|||
id: inputDialog
|
||||
|
||||
property alias prompt: promptLabel.text
|
||||
property alias echoMode: statusInput.echoMode
|
||||
property var onAccepted: undefined
|
||||
|
||||
modality: Qt.NonModal
|
||||
|
@ -21,7 +22,8 @@ ApplicationWindow {
|
|||
height: fontMetrics.lineSpacing * 7
|
||||
|
||||
ColumnLayout {
|
||||
anchors.margins: Nheko.paddingLarge
|
||||
spacing: Nheko.paddingMedium
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
anchors.fill: parent
|
||||
|
||||
Label {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
@ -34,7 +35,7 @@ ApplicationWindow {
|
|||
width: 340
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(inviteDialogRoot)
|
||||
|
||||
Shortcut {
|
78
resources/qml/dialogs/JoinRoomDialog.qml
Normal file
78
resources/qml/dialogs/JoinRoomDialog.qml
Normal file
|
@ -0,0 +1,78 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick.Layouts 1.3
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: joinRoomRoot
|
||||
|
||||
title: qsTr("Join room")
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
Component.onCompleted: Nheko.reparent(joinRoomRoot)
|
||||
width: 350
|
||||
height: fontMetrics.lineSpacing * 7
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: dbb.rejected()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Nheko.paddingMedium
|
||||
anchors.margins: Nheko.paddingMedium
|
||||
anchors.fill: parent
|
||||
|
||||
Label {
|
||||
id: promptLabel
|
||||
|
||||
text: qsTr("Room ID or alias")
|
||||
color: Nheko.colors.text
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
id: input
|
||||
|
||||
focus: true
|
||||
Layout.fillWidth: true
|
||||
onAccepted: {
|
||||
if (input.text.match("#.+?:.{3,}"))
|
||||
dbb.accepted();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
id: dbb
|
||||
|
||||
onAccepted: {
|
||||
Nheko.joinRoom(input.text);
|
||||
joinRoomRoot.close();
|
||||
}
|
||||
onRejected: {
|
||||
joinRoomRoot.close();
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Join"
|
||||
enabled: input.text.match("#.+?:.{3,}")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cancel"
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
20
resources/qml/dialogs/LeaveRoomDialog.qml
Normal file
20
resources/qml/dialogs/LeaveRoomDialog.qml
Normal file
|
@ -0,0 +1,20 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import im.nheko 1.0
|
||||
|
||||
MessageDialog {
|
||||
id: leaveRoomRoot
|
||||
|
||||
required property string roomId
|
||||
|
||||
title: qsTr("Leave room")
|
||||
text: qsTr("Are you sure you want to leave?")
|
||||
modality: Qt.ApplicationModal
|
||||
buttons: Dialog.Ok | Dialog.Cancel
|
||||
onAccepted: Rooms.leave(roomId)
|
||||
}
|
19
resources/qml/dialogs/LogoutDialog.qml
Normal file
19
resources/qml/dialogs/LogoutDialog.qml
Normal file
|
@ -0,0 +1,19 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform 1.1
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import im.nheko 1.0
|
||||
|
||||
MessageDialog {
|
||||
id: logoutRoot
|
||||
|
||||
title: qsTr("Log out")
|
||||
text: CallManager.isOnCall ? qsTr("A call is in progress. Log out?") : qsTr("Are you sure you want to log out?")
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
buttons: Dialog.Ok | Dialog.Cancel
|
||||
onAccepted: Nheko.logout()
|
||||
}
|
1744
resources/qml/dialogs/PhoneNumberInputDialog.qml
Normal file
1744
resources/qml/dialogs/PhoneNumberInputDialog.qml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@ ApplicationWindow {
|
|||
width: 420
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(rawMessageRoot)
|
||||
|
||||
Shortcut {
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
@ -19,7 +20,7 @@ ApplicationWindow {
|
|||
minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(readReceiptsRoot)
|
||||
|
||||
Shortcut {
|
217
resources/qml/dialogs/RoomDirectory.qml
Normal file
217
resources/qml/dialogs/RoomDirectory.qml
Normal file
|
@ -0,0 +1,217 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import "../ui"
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: roomDirectoryWindow
|
||||
|
||||
property RoomDirectoryModel publicRooms
|
||||
|
||||
visible: true
|
||||
minimumWidth: 650
|
||||
minimumHeight: 420
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
modality: Qt.WindowModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(roomDirectoryWindow)
|
||||
title: qsTr("Explore Public Rooms")
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: roomDirectoryWindow.close()
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: roomDirView
|
||||
|
||||
anchors.fill: parent
|
||||
model: publicRooms
|
||||
|
||||
ScrollHelper {
|
||||
flickable: parent
|
||||
anchors.fill: parent
|
||||
enabled: !Settings.mobileMode
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: roomDirDelegate
|
||||
|
||||
property color background: Nheko.colors.window
|
||||
property color importantText: Nheko.colors.text
|
||||
property color unimportantText: Nheko.colors.buttonText
|
||||
property int avatarSize: fontMetrics.lineSpacing * 4
|
||||
|
||||
color: background
|
||||
height: avatarSize + Nheko.paddingLarge
|
||||
width: ListView.view.width
|
||||
|
||||
RowLayout {
|
||||
spacing: Nheko.paddingMedium
|
||||
anchors.fill: parent
|
||||
anchors.margins: Nheko.paddingLarge
|
||||
implicitHeight: textContent.height
|
||||
|
||||
Avatar {
|
||||
id: roomAvatar
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
width: avatarSize
|
||||
height: avatarSize
|
||||
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
roomid: model.roomid
|
||||
displayName: model.name
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: textContent
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
width: parent.width - avatar.width
|
||||
Layout.preferredWidth: parent.width - avatar.width
|
||||
spacing: Nheko.paddingSmall
|
||||
|
||||
ElidedLabel {
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
color: roomDirDelegate.importantText
|
||||
elideWidth: textContent.width - numMembersRectangle.width - buttonRectangle.width
|
||||
font.pixelSize: fontMetrics.font.pixelSize * 1.1
|
||||
fullText: model.name
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: roomDescriptionRow
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
spacing: Nheko.paddingSmall
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
Layout.preferredHeight: fontMetrics.lineSpacing * 4
|
||||
|
||||
Label {
|
||||
id: roomTopic
|
||||
|
||||
color: roomDirDelegate.unimportantText
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
font.pixelSize: fontMetrics.font.pixelSize
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 2
|
||||
Layout.fillWidth: true
|
||||
text: model.topic
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
id: numMembersRectangle
|
||||
|
||||
Layout.margins: Nheko.paddingSmall
|
||||
width: roomCount.width
|
||||
|
||||
Label {
|
||||
id: roomCount
|
||||
|
||||
color: roomDirDelegate.unimportantText
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: fontMetrics.font.pixelSize
|
||||
text: model.numMembers.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: buttonRectangle
|
||||
|
||||
Layout.margins: Nheko.paddingSmall
|
||||
width: joinRoomButton.width
|
||||
|
||||
Button {
|
||||
id: joinRoomButton
|
||||
|
||||
visible: model.canJoin
|
||||
anchors.centerIn: parent
|
||||
text: "Join"
|
||||
onClicked: publicRooms.joinRoom(model.index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width
|
||||
visible: !publicRooms.reachedEndOfPagination && publicRooms.loadingMoreRooms
|
||||
// hacky but works
|
||||
height: loadingSpinner.height + 2 * Nheko.paddingLarge
|
||||
anchors.margins: Nheko.paddingLarge
|
||||
|
||||
Spinner {
|
||||
id: loadingSpinner
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: Nheko.paddingLarge
|
||||
running: visible
|
||||
foreground: Nheko.colors.mid
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
publicRooms: RoomDirectoryModel {
|
||||
}
|
||||
|
||||
header: RowLayout {
|
||||
id: searchBarLayout
|
||||
|
||||
spacing: Nheko.paddingMedium
|
||||
width: parent.width
|
||||
implicitHeight: roomSearch.height
|
||||
|
||||
MatrixTextField {
|
||||
id: roomSearch
|
||||
|
||||
focus: true
|
||||
Layout.fillWidth: true
|
||||
selectByMouse: true
|
||||
font.pixelSize: fontMetrics.font.pixelSize
|
||||
padding: Nheko.paddingMedium
|
||||
color: Nheko.colors.text
|
||||
placeholderText: qsTr("Search for public rooms")
|
||||
onTextChanged: searchTimer.restart()
|
||||
}
|
||||
|
||||
MatrixTextField {
|
||||
id: chooseServer
|
||||
|
||||
Layout.minimumWidth: 0.3 * header.width
|
||||
Layout.maximumWidth: 0.3 * header.width
|
||||
padding: Nheko.paddingMedium
|
||||
color: Nheko.colors.text
|
||||
placeholderText: qsTr("Choose custom homeserver")
|
||||
onTextChanged: publicRooms.setMatrixServer(text)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchTimer
|
||||
|
||||
interval: 350
|
||||
onTriggered: roomDirView.model.setSearchTerm(roomSearch.text)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./ui"
|
||||
import ".."
|
||||
import "../ui"
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
@ -21,7 +22,7 @@ ApplicationWindow {
|
|||
minimumHeight: 420
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(roomMembersRoot)
|
||||
|
||||
Shortcut {
|
||||
|
@ -39,6 +40,7 @@ ApplicationWindow {
|
|||
|
||||
width: 130
|
||||
height: width
|
||||
roomid: members.roomId
|
||||
displayName: members.roomName
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
url: members.avatarUrl.replace("mxc://", "image://MxcImage/")
|
|
@ -2,7 +2,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import "./ui"
|
||||
import ".."
|
||||
import "../ui"
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.3
|
||||
|
@ -20,7 +21,7 @@ ApplicationWindow {
|
|||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(roomSettingsDialog)
|
||||
title: qsTr("Room Settings")
|
||||
|
||||
|
@ -38,6 +39,7 @@ ApplicationWindow {
|
|||
|
||||
Avatar {
|
||||
url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
roomid: roomSettings.roomId
|
||||
displayName: roomSettings.roomName
|
||||
height: 130
|
||||
width: 130
|
||||
|
@ -186,7 +188,16 @@ ApplicationWindow {
|
|||
|
||||
ComboBox {
|
||||
enabled: roomSettings.canChangeJoinRules
|
||||
model: [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")]
|
||||
model: {
|
||||
let opts = [qsTr("Anyone and guests"), qsTr("Anyone"), qsTr("Invited users")];
|
||||
if (roomSettings.supportsKnocking)
|
||||
opts.push(qsTr("By knocking"));
|
||||
|
||||
if (roomSettings.supportsRestricted)
|
||||
opts.push(qsTr("Restricted by membership in other rooms"));
|
||||
|
||||
return opts;
|
||||
}
|
||||
currentIndex: roomSettings.accessJoinRules
|
||||
onActivated: {
|
||||
roomSettings.changeAccessRules(index);
|
433
resources/qml/dialogs/UserProfile.qml
Normal file
433
resources/qml/dialogs/UserProfile.qml
Normal file
|
@ -0,0 +1,433 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import ".."
|
||||
import "../device-verification"
|
||||
import "../ui"
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.13
|
||||
import im.nheko 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
// this does not work in ApplicationWindow, just in Window
|
||||
//transientParent: Nheko.mainwindow()
|
||||
|
||||
id: userProfileDialog
|
||||
|
||||
property var profile
|
||||
|
||||
height: 650
|
||||
width: 420
|
||||
minimumWidth: 150
|
||||
minimumHeight: 150
|
||||
palette: Nheko.colors
|
||||
color: Nheko.colors.window
|
||||
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
|
||||
modality: Qt.NonModal
|
||||
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
|
||||
Component.onCompleted: Nheko.reparent(userProfileDialog)
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: userProfileDialog.close()
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: devicelist
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
spacing: 8
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: profile.deviceList
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
footerPositioning: ListView.OverlayFooter
|
||||
|
||||
ScrollHelper {
|
||||
flickable: parent
|
||||
anchors.fill: parent
|
||||
enabled: !Settings.mobileMode
|
||||
}
|
||||
|
||||
header: ColumnLayout {
|
||||
id: contentL
|
||||
|
||||
width: devicelist.width
|
||||
spacing: 10
|
||||
|
||||
Avatar {
|
||||
id: displayAvatar
|
||||
|
||||
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
height: 130
|
||||
width: 130
|
||||
displayName: profile.displayName
|
||||
userid: profile.userid
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: TimelineManager.openImageOverlay(profile.avatarUrl, "")
|
||||
|
||||
ImageButton {
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: profile.isGlobalUserProfile ? qsTr("Change avatar globally.") : qsTr("Change avatar. Will only apply to this room.")
|
||||
anchors.left: displayAvatar.left
|
||||
anchors.top: displayAvatar.top
|
||||
anchors.leftMargin: Nheko.paddingMedium
|
||||
anchors.topMargin: Nheko.paddingMedium
|
||||
visible: profile.isSelf
|
||||
image: ":/icons/icons/ui/edit.png"
|
||||
onClicked: profile.changeAvatar()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spinner {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
running: profile.isLoading
|
||||
visible: profile.isLoading
|
||||
foreground: Nheko.colors.mid
|
||||
}
|
||||
|
||||
Text {
|
||||
id: errorText
|
||||
|
||||
color: "red"
|
||||
visible: opacity > 0
|
||||
opacity: 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: hideErrorAnimation
|
||||
|
||||
running: false
|
||||
|
||||
PauseAnimation {
|
||||
duration: 4000
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: errorText
|
||||
property: 'opacity'
|
||||
to: 0
|
||||
duration: 1000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDisplayError(errorMessage) {
|
||||
errorText.text = errorMessage;
|
||||
errorText.opacity = 1;
|
||||
hideErrorAnimation.restart();
|
||||
}
|
||||
|
||||
target: profile
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: displayUsername
|
||||
|
||||
property bool isUsernameEditingAllowed
|
||||
|
||||
readOnly: !isUsernameEditingAllowed
|
||||
text: profile.displayName
|
||||
font.pixelSize: 20
|
||||
color: TimelineManager.userColor(profile.userid, Nheko.colors.window)
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
selectByMouse: true
|
||||
onAccepted: {
|
||||
profile.changeUsername(displayUsername.text);
|
||||
displayUsername.isUsernameEditingAllowed = false;
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
visible: profile.isSelf
|
||||
anchors.leftMargin: Nheko.paddingSmall
|
||||
anchors.left: displayUsername.right
|
||||
anchors.verticalCenter: displayUsername.verticalCenter
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: profile.isGlobalUserProfile ? qsTr("Change display name globally.") : qsTr("Change display name. Will only apply to this room.")
|
||||
image: displayUsername.isUsernameEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
|
||||
onClicked: {
|
||||
if (displayUsername.isUsernameEditingAllowed) {
|
||||
profile.changeUsername(displayUsername.text);
|
||||
displayUsername.isUsernameEditingAllowed = false;
|
||||
} else {
|
||||
displayUsername.isUsernameEditingAllowed = true;
|
||||
displayUsername.focus = true;
|
||||
displayUsername.selectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MatrixText {
|
||||
text: profile.userid
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: !profile.isGlobalUserProfile
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Nheko.paddingSmall
|
||||
|
||||
MatrixText {
|
||||
id: displayRoomname
|
||||
|
||||
text: qsTr("Room: %1").arg(profile.room ? profile.room.roomName : "")
|
||||
ToolTip.text: qsTr("This is a room-specific profile. The user's name and avatar may be different from their global versions.")
|
||||
ToolTip.visible: ma.hovered
|
||||
|
||||
HoverHandler {
|
||||
id: ma
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/world.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Open the global profile for this user.")
|
||||
onClicked: profile.openGlobalProfile()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button {
|
||||
id: verifyUserButton
|
||||
|
||||
text: qsTr("Verify")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enabled: profile.userVerified != Crypto.Verified
|
||||
visible: profile.userVerified != Crypto.Verified && !profile.isSelf && profile.userVerificationEnabled
|
||||
onClicked: profile.verify()
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : Nheko.colors.buttonText)
|
||||
visible: profile.userVerified != Crypto.Unverified
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
// ImageButton{
|
||||
// image:":/icons/icons/ui/volume-off-indicator.png"
|
||||
// Layout.margins: {
|
||||
// left: 5
|
||||
// right: 5
|
||||
// }
|
||||
// ToolTip.visible: hovered
|
||||
// ToolTip.text: qsTr("Ignore messages from this user.")
|
||||
// onClicked : {
|
||||
// profile.ignoreUser()
|
||||
// }
|
||||
// }
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 10
|
||||
spacing: Nheko.paddingSmall
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/black-bubble-speech.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Start a private chat.")
|
||||
onClicked: profile.startChat()
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/round-remove-button.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Kick the user.")
|
||||
onClicked: profile.kickUser()
|
||||
visible: !profile.isGlobalUserProfile && profile.room.permissions.canKick()
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Ban the user.")
|
||||
onClicked: profile.banUser()
|
||||
visible: !profile.isGlobalUserProfile && profile.room.permissions.canBan()
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
image: ":/icons/icons/ui/refresh.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Refresh device list.")
|
||||
onClicked: profile.refreshDevices()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
delegate: RowLayout {
|
||||
required property int verificationStatus
|
||||
required property string deviceId
|
||||
required property string deviceName
|
||||
required property string lastIp
|
||||
required property var lastTs
|
||||
|
||||
width: devicelist.width
|
||||
spacing: 4
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
color: Nheko.colors.text
|
||||
text: deviceId
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
visible: profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE
|
||||
source: {
|
||||
switch (verificationStatus) {
|
||||
case VerificationStatus.VERIFIED:
|
||||
return "image://colorimage/:/icons/icons/ui/lock.png?green";
|
||||
case VerificationStatus.UNVERIFIED:
|
||||
return "image://colorimage/:/icons/icons/ui/unlock.png?yellow";
|
||||
case VerificationStatus.SELF:
|
||||
return "image://colorimage/:/icons/icons/ui/checkmark.png?green";
|
||||
default:
|
||||
return "image://colorimage/:/icons/icons/ui/unlock.png?red";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
image: ":/icons/icons/ui/power-button-off.png"
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Sign out this device.")
|
||||
onClicked: profile.signOutDevice(deviceId)
|
||||
visible: profile.isSelf
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: deviceNameRow
|
||||
|
||||
property bool isEditingAllowed
|
||||
|
||||
TextInput {
|
||||
id: deviceNameField
|
||||
|
||||
readOnly: !deviceNameRow.isEditingAllowed
|
||||
text: deviceName
|
||||
color: Nheko.colors.text
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
selectByMouse: true
|
||||
onAccepted: {
|
||||
profile.changeDeviceName(deviceId, deviceNameField.text);
|
||||
deviceNameRow.isEditingAllowed = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton {
|
||||
visible: profile.isSelf
|
||||
hoverEnabled: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Change device name.")
|
||||
image: deviceNameRow.isEditingAllowed ? ":/icons/icons/ui/checkmark.png" : ":/icons/icons/ui/edit.png"
|
||||
onClicked: {
|
||||
if (deviceNameRow.isEditingAllowed) {
|
||||
profile.changeDeviceName(deviceId, deviceNameField.text);
|
||||
deviceNameRow.isEditingAllowed = false;
|
||||
} else {
|
||||
deviceNameRow.isEditingAllowed = true;
|
||||
deviceNameField.focus = true;
|
||||
deviceNameField.selectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: profile.isSelf
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
elide: Text.ElideRight
|
||||
color: Nheko.colors.text
|
||||
text: qsTr("Last seen %1 from %2").arg(new Date(lastTs).toLocaleString(Locale.ShortFormat)).arg(lastIp ? lastIp : "???")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 16
|
||||
Layout.preferredWidth: 16
|
||||
visible: !profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE
|
||||
source: {
|
||||
switch (verificationStatus) {
|
||||
case VerificationStatus.VERIFIED:
|
||||
return "image://colorimage/:/icons/icons/ui/lock.png?green";
|
||||
case VerificationStatus.UNVERIFIED:
|
||||
return "image://colorimage/:/icons/icons/ui/unlock.png?yellow";
|
||||
case VerificationStatus.SELF:
|
||||
return "image://colorimage/:/icons/icons/ui/checkmark.png?green";
|
||||
default:
|
||||
return "image://colorimage/:/icons/icons/ui/unlock.png?red";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: verifyButton
|
||||
|
||||
visible: verificationStatus == VerificationStatus.UNVERIFIED && (profile.isSelf || !profile.userVerificationEnabled)
|
||||
text: (verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
|
||||
onClicked: {
|
||||
if (verificationStatus == VerificationStatus.VERIFIED)
|
||||
profile.unverify(deviceId);
|
||||
else
|
||||
profile.verify(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
z: 2
|
||||
width: devicelist.width
|
||||
alignment: Qt.AlignRight
|
||||
standardButtons: DialogButtonBox.Ok
|
||||
onAccepted: userProfileDialog.close()
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Nheko.colors.window
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -72,7 +72,8 @@ Menu {
|
|||
onVisibleChanged: {
|
||||
if (visible)
|
||||
forceActiveFocus();
|
||||
|
||||
else
|
||||
clear();
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
|
|
@ -34,14 +34,15 @@ Rectangle {
|
|||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: CallManager.callParty
|
||||
userid: CallManager.callParty
|
||||
displayName: CallManager.callPartyDisplayName
|
||||
onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.leftMargin: 8
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
text: CallManager.callParty
|
||||
text: CallManager.callPartyDisplayName
|
||||
color: "#000000"
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ Popup {
|
|||
Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: msgView.height / 25
|
||||
text: CallManager.callParty
|
||||
text: CallManager.callPartyDisplayName
|
||||
font.pointSize: fontMetrics.font.pointSize * 2
|
||||
color: Nheko.colors.windowText
|
||||
}
|
||||
|
@ -50,7 +50,8 @@ Popup {
|
|||
width: msgView.height / 5
|
||||
height: msgView.height / 5
|
||||
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: CallManager.callParty
|
||||
userid: CallManager.callParty
|
||||
displayName: CallManager.callPartyDisplayName
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
|
|
@ -41,14 +41,15 @@ Rectangle {
|
|||
width: Nheko.avatarSize
|
||||
height: Nheko.avatarSize
|
||||
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: CallManager.callParty
|
||||
userid: CallManager.callParty
|
||||
displayName: CallManager.callPartyDisplayName
|
||||
onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.leftMargin: 8
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||
text: CallManager.callParty
|
||||
text: CallManager.callPartyDisplayName
|
||||
color: "#000000"
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ Popup {
|
|||
height: Nheko.avatarSize
|
||||
url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
|
||||
displayName: room.roomName
|
||||
roomid: room.roomid
|
||||
onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
|
||||
}
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
<file>icons/ui/screen-share.png</file>
|
||||
<file>icons/ui/toggle-camera-view.png</file>
|
||||
<file>icons/ui/video-call.png</file>
|
||||
<file>icons/ui/refresh.png</file>
|
||||
<file>icons/emoji-categories/people.png</file>
|
||||
<file>icons/emoji-categories/people@2x.png</file>
|
||||
<file>icons/emoji-categories/nature.png</file>
|
||||
|
@ -138,31 +139,47 @@
|
|||
<file>qml/TopBar.qml</file>
|
||||
<file>qml/QuickSwitcher.qml</file>
|
||||
<file>qml/ForwardCompleter.qml</file>
|
||||
<file>qml/SelfVerificationCheck.qml</file>
|
||||
<file>qml/TypingIndicator.qml</file>
|
||||
<file>qml/RoomSettings.qml</file>
|
||||
<file>qml/emoji/EmojiPicker.qml</file>
|
||||
<file>qml/emoji/StickerPicker.qml</file>
|
||||
<file>qml/UserProfile.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/NotificationWarning.qml</file>
|
||||
<file>qml/components/AdaptiveLayout.qml</file>
|
||||
<file>qml/components/AdaptiveLayoutElement.qml</file>
|
||||
<file>qml/components/AvatarListTile.qml</file>
|
||||
<file>qml/components/FlatButton.qml</file>
|
||||
<file>qml/components/MainWindowDialog.qml</file>
|
||||
<file>qml/delegates/Encrypted.qml</file>
|
||||
<file>qml/delegates/FileMessage.qml</file>
|
||||
<file>qml/delegates/ImageMessage.qml</file>
|
||||
<file>qml/delegates/MessageDelegate.qml</file>
|
||||
<file>qml/delegates/NoticeMessage.qml</file>
|
||||
<file>qml/delegates/Pill.qml</file>
|
||||
<file>qml/delegates/Placeholder.qml</file>
|
||||
<file>qml/delegates/PlayableMediaMessage.qml</file>
|
||||
<file>qml/delegates/Reply.qml</file>
|
||||
<file>qml/delegates/TextMessage.qml</file>
|
||||
<file>qml/device-verification/Waiting.qml</file>
|
||||
<file>qml/device-verification/DeviceVerification.qml</file>
|
||||
<file>qml/device-verification/DigitVerification.qml</file>
|
||||
<file>qml/device-verification/EmojiVerification.qml</file>
|
||||
<file>qml/device-verification/NewVerificationRequest.qml</file>
|
||||
<file>qml/device-verification/Failed.qml</file>
|
||||
<file>qml/device-verification/NewVerificationRequest.qml</file>
|
||||
<file>qml/device-verification/Success.qml</file>
|
||||
<file>qml/dialogs/InputDialog.qml</file>
|
||||
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
||||
<file>qml/device-verification/Waiting.qml</file>
|
||||
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
|
||||
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
|
||||
<file>qml/dialogs/PhoneNumberInputDialog.qml</file>
|
||||
<file>qml/dialogs/InputDialog.qml</file>
|
||||
<file>qml/dialogs/InviteDialog.qml</file>
|
||||
<file>qml/dialogs/JoinRoomDialog.qml</file>
|
||||
<file>qml/dialogs/LeaveRoomDialog.qml</file>
|
||||
<file>qml/dialogs/LogoutDialog.qml</file>
|
||||
<file>qml/dialogs/RawMessageDialog.qml</file>
|
||||
<file>qml/dialogs/ReadReceipts.qml</file>
|
||||
<file>qml/dialogs/RoomDirectory.qml</file>
|
||||
<file>qml/dialogs/RoomMembers.qml</file>
|
||||
<file>qml/dialogs/RoomSettings.qml</file>
|
||||
<file>qml/dialogs/UserProfile.qml</file>
|
||||
<file>qml/emoji/EmojiPicker.qml</file>
|
||||
<file>qml/emoji/StickerPicker.qml</file>
|
||||
<file>qml/ui/Ripple.qml</file>
|
||||
<file>qml/ui/Spinner.qml</file>
|
||||
<file>qml/ui/animations/BlinkAnimation.qml</file>
|
||||
|
@ -174,14 +191,6 @@
|
|||
<file>qml/voip/PlaceCall.qml</file>
|
||||
<file>qml/voip/ScreenShare.qml</file>
|
||||
<file>qml/voip/VideoCall.qml</file>
|
||||
<file>qml/components/AdaptiveLayout.qml</file>
|
||||
<file>qml/components/AdaptiveLayoutElement.qml</file>
|
||||
<file>qml/components/AvatarListTile.qml</file>
|
||||
<file>qml/components/FlatButton.qml</file>
|
||||
<file>qml/RoomMembers.qml</file>
|
||||
<file>qml/InviteDialog.qml</file>
|
||||
<file>qml/ReadReceipts.qml</file>
|
||||
<file>qml/RawMessageDialog.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/media">
|
||||
<file>media/ring.ogg</file>
|
||||
|
|
|
@ -54,7 +54,7 @@ if __name__ == '__main__':
|
|||
}
|
||||
|
||||
current_category = ''
|
||||
for line in open(filename, 'r'):
|
||||
for line in open(filename, 'r', encoding="utf8"):
|
||||
if line.startswith('# group:'):
|
||||
current_category = line.split(':', 1)[1].strip()
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
6328
src/Cache.cpp
6328
src/Cache.cpp
File diff suppressed because it is too large
Load diff
|
@ -83,6 +83,9 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
|
|||
//! Retrieve member info from a room.
|
||||
std::vector<RoomMember>
|
||||
getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30);
|
||||
//! Retrive member info from an invite.
|
||||
std::vector<RoomMember>
|
||||
getMembersFromInvite(const std::string &room_id, std::size_t start_index = 0, std::size_t len = 30);
|
||||
|
||||
bool
|
||||
isInitialized();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
1169
src/Cache_p.h
1169
src/Cache_p.h
File diff suppressed because it is too large
Load diff
|
@ -1,396 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#include "CallDevices.h"
|
||||
#include "ChatPage.h"
|
||||
#include "Logging.h"
|
||||
#include "UserSettingsPage.h"
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
extern "C"
|
||||
{
|
||||
#include "gst/gst.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
CallDevices::CallDevices()
|
||||
: QObject()
|
||||
{}
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
namespace {
|
||||
|
||||
struct AudioSource
|
||||
{
|
||||
std::string name;
|
||||
GstDevice *device;
|
||||
};
|
||||
|
||||
struct VideoSource
|
||||
{
|
||||
struct Caps
|
||||
{
|
||||
std::string resolution;
|
||||
std::vector<std::string> frameRates;
|
||||
};
|
||||
std::string name;
|
||||
GstDevice *device;
|
||||
std::vector<Caps> caps;
|
||||
};
|
||||
|
||||
std::vector<AudioSource> audioSources_;
|
||||
std::vector<VideoSource> videoSources_;
|
||||
|
||||
using FrameRate = std::pair<int, int>;
|
||||
std::optional<FrameRate>
|
||||
getFrameRate(const GValue *value)
|
||||
{
|
||||
if (GST_VALUE_HOLDS_FRACTION(value)) {
|
||||
gint num = gst_value_get_fraction_numerator(value);
|
||||
gint den = gst_value_get_fraction_denominator(value);
|
||||
return FrameRate{num, den};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
addFrameRate(std::vector<std::string> &rates, const FrameRate &rate)
|
||||
{
|
||||
constexpr double minimumFrameRate = 15.0;
|
||||
if (static_cast<double>(rate.first) / rate.second >= minimumFrameRate)
|
||||
rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second));
|
||||
}
|
||||
|
||||
void
|
||||
setDefaultDevice(bool isVideo)
|
||||
{
|
||||
auto settings = ChatPage::instance()->userSettings();
|
||||
if (isVideo && settings->camera().isEmpty()) {
|
||||
const VideoSource &camera = videoSources_.front();
|
||||
settings->setCamera(QString::fromStdString(camera.name));
|
||||
settings->setCameraResolution(
|
||||
QString::fromStdString(camera.caps.front().resolution));
|
||||
settings->setCameraFrameRate(
|
||||
QString::fromStdString(camera.caps.front().frameRates.front()));
|
||||
} else if (!isVideo && settings->microphone().isEmpty()) {
|
||||
settings->setMicrophone(QString::fromStdString(audioSources_.front().name));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
addDevice(GstDevice *device)
|
||||
{
|
||||
if (!device)
|
||||
return;
|
||||
|
||||
gchar *name = gst_device_get_display_name(device);
|
||||
gchar *type = gst_device_get_device_class(device);
|
||||
bool isVideo = !std::strncmp(type, "Video", 5);
|
||||
g_free(type);
|
||||
nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name);
|
||||
if (!isVideo) {
|
||||
audioSources_.push_back({name, device});
|
||||
g_free(name);
|
||||
setDefaultDevice(false);
|
||||
return;
|
||||
}
|
||||
|
||||
GstCaps *gstcaps = gst_device_get_caps(device);
|
||||
if (!gstcaps) {
|
||||
nhlog::ui()->debug("WebRTC: unable to get caps for {}", name);
|
||||
g_free(name);
|
||||
return;
|
||||
}
|
||||
|
||||
VideoSource source{name, device, {}};
|
||||
g_free(name);
|
||||
guint nCaps = gst_caps_get_size(gstcaps);
|
||||
for (guint i = 0; i < nCaps; ++i) {
|
||||
GstStructure *structure = gst_caps_get_structure(gstcaps, i);
|
||||
const gchar *struct_name = gst_structure_get_name(structure);
|
||||
if (!std::strcmp(struct_name, "video/x-raw")) {
|
||||
gint widthpx, heightpx;
|
||||
if (gst_structure_get(structure,
|
||||
"width",
|
||||
G_TYPE_INT,
|
||||
&widthpx,
|
||||
"height",
|
||||
G_TYPE_INT,
|
||||
&heightpx,
|
||||
nullptr)) {
|
||||
VideoSource::Caps caps;
|
||||
caps.resolution =
|
||||
std::to_string(widthpx) + "x" + std::to_string(heightpx);
|
||||
const GValue *value =
|
||||
gst_structure_get_value(structure, "framerate");
|
||||
if (auto fr = getFrameRate(value); fr)
|
||||
addFrameRate(caps.frameRates, *fr);
|
||||
else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) {
|
||||
addFrameRate(
|
||||
caps.frameRates,
|
||||
*getFrameRate(gst_value_get_fraction_range_min(value)));
|
||||
addFrameRate(
|
||||
caps.frameRates,
|
||||
*getFrameRate(gst_value_get_fraction_range_max(value)));
|
||||
} else if (GST_VALUE_HOLDS_LIST(value)) {
|
||||
guint nRates = gst_value_list_get_size(value);
|
||||
for (guint j = 0; j < nRates; ++j) {
|
||||
const GValue *rate =
|
||||
gst_value_list_get_value(value, j);
|
||||
if (auto frate = getFrameRate(rate); frate)
|
||||
addFrameRate(caps.frameRates, *frate);
|
||||
}
|
||||
}
|
||||
if (!caps.frameRates.empty())
|
||||
source.caps.push_back(std::move(caps));
|
||||
}
|
||||
}
|
||||
}
|
||||
gst_caps_unref(gstcaps);
|
||||
videoSources_.push_back(std::move(source));
|
||||
setDefaultDevice(true);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool
|
||||
removeDevice(T &sources, GstDevice *device, bool changed)
|
||||
{
|
||||
if (auto it = std::find_if(sources.begin(),
|
||||
sources.end(),
|
||||
[device](const auto &s) { return s.device == device; });
|
||||
it != sources.end()) {
|
||||
nhlog::ui()->debug(std::string("WebRTC: device ") +
|
||||
(changed ? "changed: " : "removed: ") + "{}",
|
||||
it->name);
|
||||
gst_object_unref(device);
|
||||
sources.erase(it);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
removeDevice(GstDevice *device, bool changed)
|
||||
{
|
||||
if (device) {
|
||||
if (removeDevice(audioSources_, device, changed) ||
|
||||
removeDevice(videoSources_, device, changed))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data G_GNUC_UNUSED)
|
||||
{
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_DEVICE_ADDED: {
|
||||
GstDevice *device;
|
||||
gst_message_parse_device_added(msg, &device);
|
||||
addDevice(device);
|
||||
emit CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_REMOVED: {
|
||||
GstDevice *device;
|
||||
gst_message_parse_device_removed(msg, &device);
|
||||
removeDevice(device, false);
|
||||
emit CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_CHANGED: {
|
||||
GstDevice *device;
|
||||
GstDevice *oldDevice;
|
||||
gst_message_parse_device_changed(msg, &device, &oldDevice);
|
||||
removeDevice(oldDevice, true);
|
||||
addDevice(device);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<std::string>
|
||||
deviceNames(T &sources, const std::string &defaultDevice)
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
ret.reserve(sources.size());
|
||||
for (const auto &s : sources)
|
||||
ret.push_back(s.name);
|
||||
|
||||
// move default device to top of the list
|
||||
if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end())
|
||||
std::swap(ret.front(), *it);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::optional<VideoSource>
|
||||
getVideoSource(const std::string &cameraName)
|
||||
{
|
||||
if (auto it = std::find_if(videoSources_.cbegin(),
|
||||
videoSources_.cend(),
|
||||
[&cameraName](const auto &s) { return s.name == cameraName; });
|
||||
it != videoSources_.cend()) {
|
||||
return *it;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::pair<int, int>
|
||||
tokenise(std::string_view str, char delim)
|
||||
{
|
||||
std::pair<int, int> ret;
|
||||
ret.first = std::atoi(str.data());
|
||||
auto pos = str.find_first_of(delim);
|
||||
ret.second = std::atoi(str.data() + pos + 1);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CallDevices::init()
|
||||
{
|
||||
static GstDeviceMonitor *monitor = nullptr;
|
||||
if (!monitor) {
|
||||
monitor = gst_device_monitor_new();
|
||||
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
|
||||
gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
|
||||
gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps);
|
||||
gst_caps_unref(caps);
|
||||
caps = gst_caps_new_empty_simple("video/x-raw");
|
||||
gst_device_monitor_add_filter(monitor, "Video/Source", caps);
|
||||
gst_device_monitor_add_filter(monitor, "Video/Duplex", caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
GstBus *bus = gst_device_monitor_get_bus(monitor);
|
||||
gst_bus_add_watch(bus, newBusMessage, nullptr);
|
||||
gst_object_unref(bus);
|
||||
if (!gst_device_monitor_start(monitor)) {
|
||||
nhlog::ui()->error("WebRTC: failed to start device monitor");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
CallDevices::haveMic() const
|
||||
{
|
||||
return !audioSources_.empty();
|
||||
}
|
||||
|
||||
bool
|
||||
CallDevices::haveCamera() const
|
||||
{
|
||||
return !videoSources_.empty();
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
CallDevices::names(bool isVideo, const std::string &defaultDevice) const
|
||||
{
|
||||
return isVideo ? deviceNames(videoSources_, defaultDevice)
|
||||
: deviceNames(audioSources_, defaultDevice);
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
CallDevices::resolutions(const std::string &cameraName) const
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
if (auto s = getVideoSource(cameraName); s) {
|
||||
ret.reserve(s->caps.size());
|
||||
for (const auto &c : s->caps)
|
||||
ret.push_back(c.resolution);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
CallDevices::frameRates(const std::string &cameraName, const std::string &resolution) const
|
||||
{
|
||||
if (auto s = getVideoSource(cameraName); s) {
|
||||
if (auto it =
|
||||
std::find_if(s->caps.cbegin(),
|
||||
s->caps.cend(),
|
||||
[&](const auto &c) { return c.resolution == resolution; });
|
||||
it != s->caps.cend())
|
||||
return it->frameRates;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GstDevice *
|
||||
CallDevices::audioDevice() const
|
||||
{
|
||||
std::string name = ChatPage::instance()->userSettings()->microphone().toStdString();
|
||||
if (auto it = std::find_if(audioSources_.cbegin(),
|
||||
audioSources_.cend(),
|
||||
[&name](const auto &s) { return s.name == name; });
|
||||
it != audioSources_.cend()) {
|
||||
nhlog::ui()->debug("WebRTC: microphone: {}", name);
|
||||
return it->device;
|
||||
} else {
|
||||
nhlog::ui()->error("WebRTC: unknown microphone: {}", name);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
GstDevice *
|
||||
CallDevices::videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const
|
||||
{
|
||||
auto settings = ChatPage::instance()->userSettings();
|
||||
std::string name = settings->camera().toStdString();
|
||||
if (auto s = getVideoSource(name); s) {
|
||||
nhlog::ui()->debug("WebRTC: camera: {}", name);
|
||||
resolution = tokenise(settings->cameraResolution().toStdString(), 'x');
|
||||
frameRate = tokenise(settings->cameraFrameRate().toStdString(), '/');
|
||||
nhlog::ui()->debug(
|
||||
"WebRTC: camera resolution: {}x{}", resolution.first, resolution.second);
|
||||
nhlog::ui()->debug(
|
||||
"WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second);
|
||||
return s->device;
|
||||
} else {
|
||||
nhlog::ui()->error("WebRTC: unknown camera: {}", name);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool
|
||||
CallDevices::haveMic() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
CallDevices::haveCamera() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
CallDevices::names(bool, const std::string &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
CallDevices::resolutions(const std::string &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
CallDevices::frameRates(const std::string &, const std::string &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
#endif
|
|
@ -1,48 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
typedef struct _GstDevice GstDevice;
|
||||
|
||||
class CallDevices : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static CallDevices &instance()
|
||||
{
|
||||
static CallDevices instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool haveMic() const;
|
||||
bool haveCamera() const;
|
||||
std::vector<std::string> names(bool isVideo, const std::string &defaultDevice) const;
|
||||
std::vector<std::string> resolutions(const std::string &cameraName) const;
|
||||
std::vector<std::string> frameRates(const std::string &cameraName,
|
||||
const std::string &resolution) const;
|
||||
|
||||
signals:
|
||||
void devicesChanged();
|
||||
|
||||
private:
|
||||
CallDevices();
|
||||
|
||||
friend class WebRTCSession;
|
||||
void init();
|
||||
GstDevice *audioDevice() const;
|
||||
GstDevice *videoDevice(std::pair<int, int> &resolution,
|
||||
std::pair<int, int> &frameRate) const;
|
||||
|
||||
public:
|
||||
CallDevices(CallDevices const &) = delete;
|
||||
void operator=(CallDevices const &) = delete;
|
||||
};
|
|
@ -1,689 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#include <QMediaPlaylist>
|
||||
#include <QUrl>
|
||||
|
||||
#include "Cache.h"
|
||||
#include "CallDevices.h"
|
||||
#include "CallManager.h"
|
||||
#include "ChatPage.h"
|
||||
#include "Logging.h"
|
||||
#include "MatrixClient.h"
|
||||
#include "UserSettingsPage.h"
|
||||
#include "Utils.h"
|
||||
|
||||
#include "mtx/responses/turn_server.hpp"
|
||||
|
||||
#ifdef XCB_AVAILABLE
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_ewmh.h>
|
||||
#endif
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
extern "C"
|
||||
{
|
||||
#include "gst/gst.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
|
||||
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
|
||||
Q_DECLARE_METATYPE(mtx::responses::TurnServer)
|
||||
|
||||
using namespace mtx::events;
|
||||
using namespace mtx::events::msg;
|
||||
|
||||
using webrtc::CallType;
|
||||
|
||||
namespace {
|
||||
std::vector<std::string>
|
||||
getTurnURIs(const mtx::responses::TurnServer &turnServer);
|
||||
}
|
||||
|
||||
CallManager::CallManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, session_(WebRTCSession::instance())
|
||||
, turnServerTimer_(this)
|
||||
{
|
||||
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
|
||||
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
|
||||
qRegisterMetaType<mtx::responses::TurnServer>();
|
||||
|
||||
connect(
|
||||
&session_,
|
||||
&WebRTCSession::offerCreated,
|
||||
this,
|
||||
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
|
||||
emit newMessage(roomid_, CallInvite{callid_, sdp, "0", timeoutms_});
|
||||
emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
|
||||
std::string callid(callid_);
|
||||
QTimer::singleShot(timeoutms_, this, [this, callid]() {
|
||||
if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) {
|
||||
hangUp(CallHangUp::Reason::InviteTimeOut);
|
||||
emit ChatPage::instance()->showNotification(
|
||||
"The remote side failed to pick up.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
connect(
|
||||
&session_,
|
||||
&WebRTCSession::answerCreated,
|
||||
this,
|
||||
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
|
||||
emit newMessage(roomid_, CallAnswer{callid_, sdp, "0"});
|
||||
emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"});
|
||||
});
|
||||
|
||||
connect(&session_,
|
||||
&WebRTCSession::newICECandidate,
|
||||
this,
|
||||
[this](const CallCandidates::Candidate &candidate) {
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
|
||||
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, "0"});
|
||||
});
|
||||
|
||||
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
|
||||
|
||||
connect(this,
|
||||
&CallManager::turnServerRetrieved,
|
||||
this,
|
||||
[this](const mtx::responses::TurnServer &res) {
|
||||
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
|
||||
nhlog::net()->info("username: {}", res.username);
|
||||
nhlog::net()->info("ttl: {} seconds", res.ttl);
|
||||
for (const auto &u : res.uris)
|
||||
nhlog::net()->info("uri: {}", u);
|
||||
|
||||
// Request new credentials close to expiry
|
||||
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||
turnURIs_ = getTurnURIs(res);
|
||||
uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
|
||||
if (res.ttl < 3600)
|
||||
nhlog::net()->warn("Setting ttl to 1 hour");
|
||||
turnServerTimer_.setInterval(ttl * 1000 * 0.9);
|
||||
});
|
||||
|
||||
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
|
||||
switch (state) {
|
||||
case webrtc::State::DISCONNECTED:
|
||||
playRingtone(QUrl("qrc:/media/media/callend.ogg"), false);
|
||||
clear();
|
||||
break;
|
||||
case webrtc::State::ICEFAILED: {
|
||||
QString error("Call connection failed.");
|
||||
if (turnURIs_.empty())
|
||||
error += " Your homeserver has no configured TURN server.";
|
||||
emit ChatPage::instance()->showNotification(error);
|
||||
hangUp(CallHangUp::Reason::ICEFailed);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
emit newCallState();
|
||||
});
|
||||
|
||||
connect(&CallDevices::instance(),
|
||||
&CallDevices::devicesChanged,
|
||||
this,
|
||||
&CallManager::devicesChanged);
|
||||
|
||||
connect(&player_,
|
||||
&QMediaPlayer::mediaStatusChanged,
|
||||
this,
|
||||
[this](QMediaPlayer::MediaStatus status) {
|
||||
if (status == QMediaPlayer::LoadedMedia)
|
||||
player_.play();
|
||||
});
|
||||
|
||||
connect(&player_,
|
||||
QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error),
|
||||
[this](QMediaPlayer::Error error) {
|
||||
stopRingtone();
|
||||
switch (error) {
|
||||
case QMediaPlayer::FormatError:
|
||||
case QMediaPlayer::ResourceError:
|
||||
nhlog::ui()->error("WebRTC: valid ringtone file not found");
|
||||
break;
|
||||
case QMediaPlayer::AccessDeniedError:
|
||||
nhlog::ui()->error("WebRTC: access to ringtone file denied");
|
||||
break;
|
||||
default:
|
||||
nhlog::ui()->error("WebRTC: unable to play ringtone");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
|
||||
{
|
||||
if (isOnCall())
|
||||
return;
|
||||
if (callType == CallType::SCREEN) {
|
||||
if (!screenShareSupported())
|
||||
return;
|
||||
if (windows_.empty() || windowIndex >= windows_.size()) {
|
||||
nhlog::ui()->error("WebRTC: window index out of range");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
|
||||
if (roomInfo.member_count != 2) {
|
||||
emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string errorMessage;
|
||||
if (!session_.havePlugins(false, &errorMessage) ||
|
||||
((callType == CallType::VIDEO || callType == CallType::SCREEN) &&
|
||||
!session_.havePlugins(true, &errorMessage))) {
|
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
callType_ = callType;
|
||||
roomid_ = roomid;
|
||||
session_.setTurnServers(turnURIs_);
|
||||
generateCallID();
|
||||
std::string strCallType = callType_ == CallType::VOICE
|
||||
? "voice"
|
||||
: (callType_ == CallType::VIDEO ? "video" : "screen");
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType);
|
||||
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
|
||||
const RoomMember &callee =
|
||||
members.front().user_id == utils::localUser() ? members.back() : members.front();
|
||||
callParty_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
|
||||
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
|
||||
emit newInviteState();
|
||||
playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true);
|
||||
if (!session_.createOffer(
|
||||
callType, callType == CallType::SCREEN ? windows_[windowIndex].second : 0)) {
|
||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||
endCall();
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string
|
||||
callHangUpReasonString(CallHangUp::Reason reason)
|
||||
{
|
||||
switch (reason) {
|
||||
case CallHangUp::Reason::ICEFailed:
|
||||
return "ICE failed";
|
||||
case CallHangUp::Reason::InviteTimeOut:
|
||||
return "Invite time out";
|
||||
default:
|
||||
return "User";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::hangUp(CallHangUp::Reason reason)
|
||||
{
|
||||
if (!callid_.empty()) {
|
||||
nhlog::ui()->debug(
|
||||
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
|
||||
emit newMessage(roomid_, CallHangUp{callid_, "0", reason});
|
||||
endCall();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
|
||||
{
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) ||
|
||||
handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event))
|
||||
return;
|
||||
#else
|
||||
(void)event;
|
||||
#endif
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool
|
||||
CallManager::handleEvent(const mtx::events::collections::TimelineEvents &event)
|
||||
{
|
||||
if (std::holds_alternative<RoomEvent<T>>(event)) {
|
||||
handleEvent(std::get<RoomEvent<T>>(event));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
|
||||
{
|
||||
const char video[] = "m=video";
|
||||
const std::string &sdp = callInviteEvent.content.sdp;
|
||||
bool isVideo = std::search(sdp.cbegin(),
|
||||
sdp.cend(),
|
||||
std::cbegin(video),
|
||||
std::cend(video) - 1,
|
||||
[](unsigned char c1, unsigned char c2) {
|
||||
return std::tolower(c1) == std::tolower(c2);
|
||||
}) != sdp.cend();
|
||||
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
|
||||
callInviteEvent.content.call_id,
|
||||
(isVideo ? "video" : "voice"),
|
||||
callInviteEvent.sender);
|
||||
|
||||
if (callInviteEvent.content.call_id.empty())
|
||||
return;
|
||||
|
||||
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
|
||||
if (isOnCall() || roomInfo.member_count != 2) {
|
||||
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
|
||||
CallHangUp{callInviteEvent.content.call_id,
|
||||
"0",
|
||||
CallHangUp::Reason::InviteTimeOut});
|
||||
return;
|
||||
}
|
||||
|
||||
const QString &ringtone = ChatPage::instance()->userSettings()->ringtone();
|
||||
if (ringtone != "Mute")
|
||||
playRingtone(ringtone == "Default" ? QUrl("qrc:/media/media/ring.ogg")
|
||||
: QUrl::fromLocalFile(ringtone),
|
||||
true);
|
||||
roomid_ = QString::fromStdString(callInviteEvent.room_id);
|
||||
callid_ = callInviteEvent.content.call_id;
|
||||
remoteICECandidates_.clear();
|
||||
|
||||
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
|
||||
const RoomMember &caller =
|
||||
members.front().user_id == utils::localUser() ? members.back() : members.front();
|
||||
callParty_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
|
||||
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
|
||||
|
||||
haveCallInvite_ = true;
|
||||
callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
|
||||
inviteSDP_ = callInviteEvent.content.sdp;
|
||||
emit newInviteState();
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::acceptInvite()
|
||||
{
|
||||
if (!haveCallInvite_)
|
||||
return;
|
||||
|
||||
stopRingtone();
|
||||
std::string errorMessage;
|
||||
if (!session_.havePlugins(false, &errorMessage) ||
|
||||
(callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) {
|
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||
hangUp();
|
||||
return;
|
||||
}
|
||||
|
||||
session_.setTurnServers(turnURIs_);
|
||||
if (!session_.acceptOffer(inviteSDP_)) {
|
||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||
hangUp();
|
||||
return;
|
||||
}
|
||||
session_.acceptICECandidates(remoteICECandidates_);
|
||||
remoteICECandidates_.clear();
|
||||
haveCallInvite_ = false;
|
||||
emit newInviteState();
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
|
||||
{
|
||||
if (callCandidatesEvent.sender == utils::localUser().toStdString())
|
||||
return;
|
||||
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
|
||||
callCandidatesEvent.content.call_id,
|
||||
callCandidatesEvent.sender);
|
||||
|
||||
if (callid_ == callCandidatesEvent.content.call_id) {
|
||||
if (isOnCall())
|
||||
session_.acceptICECandidates(callCandidatesEvent.content.candidates);
|
||||
else {
|
||||
// CallInvite has been received and we're awaiting localUser to accept or
|
||||
// reject the call
|
||||
for (const auto &c : callCandidatesEvent.content.candidates)
|
||||
remoteICECandidates_.push_back(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
|
||||
{
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
|
||||
callAnswerEvent.content.call_id,
|
||||
callAnswerEvent.sender);
|
||||
|
||||
if (callAnswerEvent.sender == utils::localUser().toStdString() &&
|
||||
callid_ == callAnswerEvent.content.call_id) {
|
||||
if (!isOnCall()) {
|
||||
emit ChatPage::instance()->showNotification(
|
||||
"Call answered on another device.");
|
||||
stopRingtone();
|
||||
haveCallInvite_ = false;
|
||||
emit newInviteState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
|
||||
stopRingtone();
|
||||
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
|
||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||
hangUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
|
||||
{
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
|
||||
callHangUpEvent.content.call_id,
|
||||
callHangUpReasonString(callHangUpEvent.content.reason),
|
||||
callHangUpEvent.sender);
|
||||
|
||||
if (callid_ == callHangUpEvent.content.call_id)
|
||||
endCall();
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::toggleMicMute()
|
||||
{
|
||||
session_.toggleMicMute();
|
||||
emit micMuteChanged();
|
||||
}
|
||||
|
||||
bool
|
||||
CallManager::callsSupported()
|
||||
{
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool
|
||||
CallManager::screenShareSupported()
|
||||
{
|
||||
return std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY");
|
||||
}
|
||||
|
||||
QStringList
|
||||
CallManager::devices(bool isVideo) const
|
||||
{
|
||||
QStringList ret;
|
||||
const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera()
|
||||
: ChatPage::instance()->userSettings()->microphone();
|
||||
std::vector<std::string> devices =
|
||||
CallDevices::instance().names(isVideo, defaultDevice.toStdString());
|
||||
ret.reserve(devices.size());
|
||||
std::transform(devices.cbegin(),
|
||||
devices.cend(),
|
||||
std::back_inserter(ret),
|
||||
[](const auto &d) { return QString::fromStdString(d); });
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::generateCallID()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
|
||||
callid_ = "c" + std::to_string(ms);
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::clear()
|
||||
{
|
||||
roomid_.clear();
|
||||
callParty_.clear();
|
||||
callPartyAvatarUrl_.clear();
|
||||
callid_.clear();
|
||||
callType_ = CallType::VOICE;
|
||||
haveCallInvite_ = false;
|
||||
emit newInviteState();
|
||||
inviteSDP_.clear();
|
||||
remoteICECandidates_.clear();
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::endCall()
|
||||
{
|
||||
stopRingtone();
|
||||
session_.end();
|
||||
clear();
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::refreshTurnServer()
|
||||
{
|
||||
turnURIs_.clear();
|
||||
turnServerTimer_.start(2000);
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::retrieveTurnServer()
|
||||
{
|
||||
http::client()->get_turn_server(
|
||||
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
turnServerTimer_.setInterval(5000);
|
||||
return;
|
||||
}
|
||||
emit turnServerRetrieved(res);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::playRingtone(const QUrl &ringtone, bool repeat)
|
||||
{
|
||||
static QMediaPlaylist playlist;
|
||||
playlist.clear();
|
||||
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
|
||||
: QMediaPlaylist::CurrentItemOnce);
|
||||
playlist.addMedia(ringtone);
|
||||
player_.setVolume(100);
|
||||
player_.setPlaylist(&playlist);
|
||||
}
|
||||
|
||||
void
|
||||
CallManager::stopRingtone()
|
||||
{
|
||||
player_.setPlaylist(nullptr);
|
||||
}
|
||||
|
||||
QStringList
|
||||
CallManager::windowList()
|
||||
{
|
||||
windows_.clear();
|
||||
windows_.push_back({tr("Entire screen"), 0});
|
||||
|
||||
#ifdef XCB_AVAILABLE
|
||||
std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection(
|
||||
xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); });
|
||||
if (xcb_connection_has_error(connection.get())) {
|
||||
nhlog::ui()->error("Failed to connect to X server");
|
||||
return {};
|
||||
}
|
||||
|
||||
xcb_ewmh_connection_t ewmh;
|
||||
if (!xcb_ewmh_init_atoms_replies(
|
||||
&ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) {
|
||||
nhlog::ui()->error("Failed to connect to EWMH server");
|
||||
return {};
|
||||
}
|
||||
std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>>
|
||||
ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); });
|
||||
|
||||
for (int i = 0; i < ewmh.nb_screens; i++) {
|
||||
xcb_ewmh_get_windows_reply_t clients;
|
||||
if (!xcb_ewmh_get_client_list_reply(
|
||||
&ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) {
|
||||
nhlog::ui()->error("Failed to request window list");
|
||||
return {};
|
||||
}
|
||||
|
||||
for (uint32_t w = 0; w < clients.windows_len; w++) {
|
||||
xcb_window_t window = clients.windows[w];
|
||||
|
||||
std::string name;
|
||||
xcb_ewmh_get_utf8_strings_reply_t data;
|
||||
auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) {
|
||||
std::string name(r->strings, r->strings_len);
|
||||
xcb_ewmh_get_utf8_strings_reply_wipe(r);
|
||||
return name;
|
||||
};
|
||||
|
||||
xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window);
|
||||
if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr))
|
||||
name = getName(&data);
|
||||
|
||||
cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window);
|
||||
if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr))
|
||||
name = getName(&data);
|
||||
|
||||
windows_.push_back({QString::fromStdString(name), window});
|
||||
}
|
||||
xcb_ewmh_get_windows_reply_wipe(&clients);
|
||||
}
|
||||
#endif
|
||||
QStringList ret;
|
||||
ret.reserve(windows_.size());
|
||||
for (const auto &w : windows_)
|
||||
ret.append(w.first);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
namespace {
|
||||
|
||||
GstElement *pipe_ = nullptr;
|
||||
unsigned int busWatchId_ = 0;
|
||||
|
||||
gboolean
|
||||
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer G_GNUC_UNUSED)
|
||||
{
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS:
|
||||
if (pipe_) {
|
||||
gst_element_set_state(GST_ELEMENT(pipe_), GST_STATE_NULL);
|
||||
gst_object_unref(pipe_);
|
||||
pipe_ = nullptr;
|
||||
}
|
||||
if (busWatchId_) {
|
||||
g_source_remove(busWatchId_);
|
||||
busWatchId_ = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void
|
||||
CallManager::previewWindow(unsigned int index) const
|
||||
{
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
if (windows_.empty() || index >= windows_.size() || !gst_is_initialized())
|
||||
return;
|
||||
|
||||
GstElement *ximagesrc = gst_element_factory_make("ximagesrc", nullptr);
|
||||
if (!ximagesrc) {
|
||||
nhlog::ui()->error("Failed to create ximagesrc");
|
||||
return;
|
||||
}
|
||||
GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
|
||||
GstElement *videoscale = gst_element_factory_make("videoscale", nullptr);
|
||||
GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr);
|
||||
GstElement *ximagesink = gst_element_factory_make("ximagesink", nullptr);
|
||||
|
||||
g_object_set(ximagesrc, "use-damage", FALSE, nullptr);
|
||||
g_object_set(ximagesrc, "show-pointer", FALSE, nullptr);
|
||||
g_object_set(ximagesrc, "xid", windows_[index].second, nullptr);
|
||||
|
||||
GstCaps *caps = gst_caps_new_simple(
|
||||
"video/x-raw", "width", G_TYPE_INT, 480, "height", G_TYPE_INT, 360, nullptr);
|
||||
g_object_set(capsfilter, "caps", caps, nullptr);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
pipe_ = gst_pipeline_new(nullptr);
|
||||
gst_bin_add_many(
|
||||
GST_BIN(pipe_), ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr);
|
||||
if (!gst_element_link_many(
|
||||
ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr)) {
|
||||
nhlog::ui()->error("Failed to link preview window elements");
|
||||
gst_object_unref(pipe_);
|
||||
pipe_ = nullptr;
|
||||
return;
|
||||
}
|
||||
if (gst_element_set_state(pipe_, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
|
||||
nhlog::ui()->error("Unable to start preview pipeline");
|
||||
gst_object_unref(pipe_);
|
||||
pipe_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
|
||||
busWatchId_ = gst_bus_add_watch(bus, newBusMessage, nullptr);
|
||||
gst_object_unref(bus);
|
||||
#else
|
||||
(void)index;
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::vector<std::string>
|
||||
getTurnURIs(const mtx::responses::TurnServer &turnServer)
|
||||
{
|
||||
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
|
||||
// where username and password are percent-encoded
|
||||
std::vector<std::string> ret;
|
||||
for (const auto &uri : turnServer.uris) {
|
||||
if (auto c = uri.find(':'); c == std::string::npos) {
|
||||
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
|
||||
continue;
|
||||
} else {
|
||||
std::string scheme = std::string(uri, 0, c);
|
||||
if (scheme != "turn" && scheme != "turns") {
|
||||
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString encodedUri =
|
||||
QString::fromStdString(scheme) + "://" +
|
||||
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) +
|
||||
":" +
|
||||
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) +
|
||||
"@" + QString::fromStdString(std::string(uri, ++c));
|
||||
ret.push_back(encodedUri.toStdString());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QMediaPlayer>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "CallDevices.h"
|
||||
#include "WebRTCSession.h"
|
||||
#include "mtx/events/collections.hpp"
|
||||
#include "mtx/events/voip.hpp"
|
||||
|
||||
namespace mtx::responses {
|
||||
struct TurnServer;
|
||||
}
|
||||
|
||||
class QStringList;
|
||||
class QUrl;
|
||||
|
||||
class CallManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState)
|
||||
Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
|
||||
Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState)
|
||||
Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState)
|
||||
Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState)
|
||||
Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState)
|
||||
Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
|
||||
Q_PROPERTY(bool haveLocalPiP READ haveLocalPiP NOTIFY newCallState)
|
||||
Q_PROPERTY(QStringList mics READ mics NOTIFY devicesChanged)
|
||||
Q_PROPERTY(QStringList cameras READ cameras NOTIFY devicesChanged)
|
||||
Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
|
||||
Q_PROPERTY(bool screenShareSupported READ screenShareSupported CONSTANT)
|
||||
|
||||
public:
|
||||
CallManager(QObject *);
|
||||
|
||||
bool haveCallInvite() const { return haveCallInvite_; }
|
||||
bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; }
|
||||
webrtc::CallType callType() const { return callType_; }
|
||||
webrtc::State callState() const { return session_.state(); }
|
||||
QString callParty() const { return callParty_; }
|
||||
QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
|
||||
bool isMicMuted() const { return session_.isMicMuted(); }
|
||||
bool haveLocalPiP() const { return session_.haveLocalPiP(); }
|
||||
QStringList mics() const { return devices(false); }
|
||||
QStringList cameras() const { return devices(true); }
|
||||
void refreshTurnServer();
|
||||
|
||||
static bool callsSupported();
|
||||
static bool screenShareSupported();
|
||||
|
||||
public slots:
|
||||
void sendInvite(const QString &roomid, webrtc::CallType, unsigned int windowIndex = 0);
|
||||
void syncEvent(const mtx::events::collections::TimelineEvents &event);
|
||||
void toggleMicMute();
|
||||
void toggleLocalPiP() { session_.toggleLocalPiP(); }
|
||||
void acceptInvite();
|
||||
void hangUp(
|
||||
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
|
||||
QStringList windowList();
|
||||
void previewWindow(unsigned int windowIndex) const;
|
||||
|
||||
signals:
|
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
|
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
|
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
|
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
|
||||
void newInviteState();
|
||||
void newCallState();
|
||||
void micMuteChanged();
|
||||
void devicesChanged();
|
||||
void turnServerRetrieved(const mtx::responses::TurnServer &);
|
||||
|
||||
private slots:
|
||||
void retrieveTurnServer();
|
||||
|
||||
private:
|
||||
WebRTCSession &session_;
|
||||
QString roomid_;
|
||||
QString callParty_;
|
||||
QString callPartyAvatarUrl_;
|
||||
std::string callid_;
|
||||
const uint32_t timeoutms_ = 120000;
|
||||
webrtc::CallType callType_ = webrtc::CallType::VOICE;
|
||||
bool haveCallInvite_ = false;
|
||||
std::string inviteSDP_;
|
||||
std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_;
|
||||
std::vector<std::string> turnURIs_;
|
||||
QTimer turnServerTimer_;
|
||||
QMediaPlayer player_;
|
||||
std::vector<std::pair<QString, uint32_t>> windows_;
|
||||
|
||||
template<typename T>
|
||||
bool handleEvent(const mtx::events::collections::TimelineEvents &event);
|
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
|
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
|
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
|
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
|
||||
void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo);
|
||||
void generateCallID();
|
||||
QStringList devices(bool isVideo) const;
|
||||
void clear();
|
||||
void endCall();
|
||||
void playRingtone(const QUrl &ringtone, bool repeat);
|
||||
void stopRingtone();
|
||||
};
|
1867
src/ChatPage.cpp
1867
src/ChatPage.cpp
File diff suppressed because it is too large
Load diff
254
src/ChatPage.h
254
src/ChatPage.h
|
@ -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> ¬ifs);
|
||||
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> ¬ifs);
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue