diff --git a/.ci/macos/notarize.sh b/.ci/macos/notarize.sh new file mode 100755 index 00000000..ca8646be --- /dev/null +++ b/.ci/macos/notarize.sh @@ -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 \ No newline at end of file diff --git a/.clang-format b/.clang-format index 059aee19..b5e2f017 100644 --- a/.clang-format +++ b/.clang-format @@ -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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cea6be7b..1012c690 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ef4470c..fdbdaaa2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 1cf5d705..b5ed4966 100644 --- a/README.md +++ b/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 + +--- + +**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 + +--- ### 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) diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index c9caddc8..19a2ad4f 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -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: diff --git a/resources/emoji-test.txt b/resources/emoji-test.txt index d3c6d12b..dd549336 100644 --- a/resources/emoji-test.txt +++ b/resources/emoji-test.txt @@ -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 diff --git a/resources/icons/ui/refresh.png b/resources/icons/ui/refresh.png new file mode 100644 index 00000000..64268203 Binary files /dev/null and b/resources/icons/ui/refresh.png differ diff --git a/resources/icons/ui/refresh.svg b/resources/icons/ui/refresh.svg new file mode 100644 index 00000000..17c41496 --- /dev/null +++ b/resources/icons/ui/refresh.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/resources/langs/nheko_cs.ts b/resources/langs/nheko_cs.ts index 99813f81..9d5a913c 100644 --- a/resources/langs/nheko_cs.ts +++ b/resources/langs/nheko_cs.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... @@ -56,7 +56,7 @@ CallInvite - + Video Call @@ -74,7 +74,7 @@ CallInviteBar - + Video Call @@ -117,7 +117,7 @@ CallManager - + Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. - + Confirm invite - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 @@ -182,7 +182,7 @@ - + Kicked user: %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -247,33 +247,35 @@ - + Failed to restore OLM account. Please login again. + + Failed to restore save data. Please login again. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - - + + Please try to login again: %1 - + Failed to join room: %1 - + You joined the room @@ -283,7 +285,7 @@ - + Room creation failed: %1 @@ -293,7 +295,7 @@ - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close @@ -645,7 +744,7 @@ InputBar - + Select a file @@ -655,7 +754,7 @@ - + Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + + + + + LeaveRoomDialog + + + Leave room + + + + + Are you sure you want to leave? + + + LoginPage @@ -756,25 +881,25 @@ Example: https://server.my:8787 - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -784,35 +909,53 @@ Example: https://server.my:8787 - + An unknown error occured. Make sure the homeserver domain is valid. - + SSO LOGIN - + Empty password - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -862,18 +1005,23 @@ Example: https://server.my:8787 - + + Allow them in + + + + %1 answered the call. - + removed - + %1 ended the call. @@ -901,7 +1049,7 @@ Example: https://server.my:8787 - + Stickers @@ -924,7 +1072,7 @@ Example: https://server.my:8787 MessageView - + Edit @@ -944,17 +1092,19 @@ Example: https://server.my:8787 - + + &Copy - + + Copy &link location - + Re&act @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1072,33 +1232,29 @@ Example: https://server.my:8787 + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 - + Password is not long enough (min 8 chars) - + Passwords don't match - + Invalid server name @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + + + + Start a new chat @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 - + User settings @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1415,16 +1608,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1454,7 +1667,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1469,7 +1687,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1515,12 +1743,12 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 - + Select an avatar @@ -1540,8 +1768,8 @@ Example: https://server.my:8787 - - + + Failed to upload image: %s @@ -1549,21 +1777,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1618,6 +1874,121 @@ Example: https://server.my:8787 + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1670,18 +2041,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 - + Failed to encrypt event, sending aborted! - + Save image @@ -1701,7 +2072,7 @@ Example: https://server.my:8787 - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1711,7 +2082,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1721,7 +2092,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1741,12 +2122,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1756,12 +2137,12 @@ Example: https://server.my:8787 - + %1 was invited. - + %1 changed their avatar. @@ -1771,12 +2152,17 @@ Example: https://server.my:8787 - + %1 joined. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. @@ -1806,32 +2192,32 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. @@ -1850,7 +2236,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1858,12 +2244,17 @@ Example: https://server.my:8787 TimelineView - + No room open - + + No preview available + + + + %1 member(s) @@ -1888,28 +2279,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options @@ -1947,10 +2350,35 @@ Example: https://server.my:8787 + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1960,33 +2388,98 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar @@ -2009,8 +2502,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2018,7 +2511,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2028,22 +2521,22 @@ Example: https://server.my:8787 - + Group's sidebar - + Circular Avatars - + profile: %1 - + Default @@ -2068,7 +2561,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2083,6 +2576,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2111,7 +2614,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2166,7 +2669,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts @@ -2177,7 +2680,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown @@ -2189,6 +2692,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications @@ -2229,12 +2737,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2244,7 +2787,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2319,7 +2862,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -2339,17 +2882,22 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2364,12 +2912,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2389,7 +2932,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2419,14 +2962,14 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File - + @@ -2434,19 +2977,19 @@ This usually causes the application icon in the task bar to animate in some fash - - + + File Password - + Enter the passphrase to decrypt the file: - + The password cannot be empty @@ -2461,6 +3004,14 @@ This usually causes the application icon in the task bar to animate in some fash + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2586,37 +3137,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - - - - - Room ID or alias - - - - - dialogs::LeaveRoom - - - Cancel - - - - - Are you sure you want to leave? - - - dialogs::Logout @@ -2668,32 +3188,6 @@ Media size: %2 - - dialogs::ReadReceipts - - - Read receipts - - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2707,47 +3201,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2762,7 +3256,7 @@ Media size: %2 - + %1: %2 @@ -2782,27 +3276,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2810,7 +3304,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index cb5b54fb..538f67e2 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Wรคhlt... @@ -17,17 +17,17 @@ You are screen sharing - Du teilst deinen Bildschirm + Bildschirm wird geteilt Hide/Show Picture-in-Picture - Bild-in-Bild Teilen/Verstecken + Bild-in-Bild zeigen/verstecken Unmute Mic - Stummschaltung Aufheben + Mikrofon aktivieren @@ -56,7 +56,7 @@ CallInvite - + Video Call Videoanruf @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Videoanruf @@ -117,7 +117,7 @@ CallManager - + Entire screen Ganzer Bildschirm @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 Nutzer konnte nicht eingeladen werden: %1 - + Invited user: %1 Eingeladener Benutzer: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. Das Migrieren des Caches auf die aktuelle Version ist fehlgeschlagen. Das kann verschiedene Grรผnde als Ursache haben. Bitte melde den Fehler und verwende in der Zwischenzeit eine รคltere Version. Alternativ kannst du den Cache manuell lรถschen. - + Confirm join Beitritt bestรคtigen @@ -151,23 +151,23 @@ Mรถchtest du wirklich %1 beitreten? - + Room %1 created. Raum %1 erzeugt. - + Confirm invite Einladung bestรคtigen - + Do you really want to invite %1 (%2)? Nutzer %1 (%2) wirklich einladen? - + Failed to invite %1 to %2: %3 Einladung von %1 in Raum %2 fehlgeschlagen: %3 @@ -182,7 +182,7 @@ Nutzer %1 (%2) wirklich kicken? - + Kicked user: %1 Gekickter Benutzer: %1 @@ -197,7 +197,7 @@ Nutzer %1 (%2) wirklich bannen? - + Failed to ban %1 in %2: %3 %1 konnte nicht aus %2 verbannt werden: %3 @@ -217,7 +217,7 @@ Bann des Nutzers %1 (%2) wirklich aufheben? - + Failed to unban %1 in %2: %3 Verbannung von %1 aus %2 konnte nicht aufgehoben werden: %3 @@ -227,12 +227,12 @@ Verbannung aufgehoben: %1 - + Do you really want to start a private chat with %1? Mรถchtest du wirklich eine private Konversation mit %1 beginnen? - + Cache migration failed! Migration des Caches fehlgeschlagen! @@ -247,33 +247,35 @@ Der Cache auf der Festplatte wurde mit einer neueren Nheko - Version angelegt. Bitte aktualisiere Nheko oder entferne den Cache. - + Failed to restore OLM account. Please login again. Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein. + + Failed to restore save data. Please login again. Gespeicherte Nachrichten konnten nicht wiederhergestellt werden. Bitte melde Dich erneut an. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Fehler beim Setup der Verschlรผsselungsschlรผssel. Servermeldung: %1 %2. Bitte versuche es spรคter erneut. - - + + Please try to login again: %1 Bitte melde dich erneut an: %1 - + Failed to join room: %1 Konnte Raum nicht betreten: %1 - + You joined the room Du hast den Raum betreten @@ -283,7 +285,7 @@ Einladung konnte nicht zurรผckgezogen werden: %1 - + Room creation failed: %1 Raum konnte nicht erstellt werden: %1 @@ -293,7 +295,7 @@ Konnte den Raum nicht verlassen: %1 - + Failed to kick %1 from %2: %3 Kontte %1 nicht aus %2 entfernen: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets Geheimnisse entschlรผsseln @@ -362,12 +364,12 @@ Gib deinen Wiederherstellungsschlรผssel oder dein Wiederherstellungspasswort ein um deine Geheimnisse zu entschlรผsseln: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: Gib deinen Wiederherstellungsschlรผssel oder dein Wiederherstellungspasswort mit dem Namen %1 ein um deine Geheimnisse zu entschlรผsseln: - + Decryption failed Entschlรผsseln fehlgeschlagen @@ -431,7 +433,7 @@ Suche - + People Leute @@ -494,6 +496,49 @@ Sie stimmen รผberein! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Kein Schlรผssel fรผr diese Nachricht vorhanden. Wir haben den Schlรผssel automatisch angefragt, aber wenn du ungeduldig bist, kannst du den Schlรผssel nocheinmal anfragen. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Diese Nachricht konnte nicht entschlรผsselt werden, weil unser Schlรผssel nur fรผr neuere Nachrichten gรผltig ist. Du kannst den Schlรผssel fรผr รคltere Nachrichten anfragen. + + + + There was an internal error reading the decryption key from the database. + Es ist ein interner Fehler beim Laden des Schlรผssels aus der Datenbank aufgetreten. + + + + There was an error decrypting this message. + Beim Entschlรผsseln der Nachricht ist ein Fehler aufgetreten. + + + + The message couldn't be parsed. + Nheko hat die Nachricht nach der Entschlรผsselung nicht verstanden. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + Der Schlรผssel fรผr diese Nachricht wurde schon einmal verwendet! Vermutlich versucht jemand falsche Nachrichten in diese Unterhaltung einzufรผgen! + + + + Unknown decryption error + Unbekannter Entschlรผsselungsfehler + + + + Request key + Schlรผssel anfragen + + EncryptionIndicator @@ -513,53 +558,8 @@ - Encrypted by an unverified device - Von einem unverifizierten Gerรคt verschlรผsselt - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Verschlรผsseltes Event (keine Schlรผssel zur Entschlรผsselung gefunden) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - -- Verschlรผsseltes Event (Schlรผssel passt nicht fรผr diesen Nachrichtenindex) -- - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- Entschlรผsselungsfehler (Fehler bei Suche nach Megolm Schlรผsseln in Datenbank) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Entschlรผsselungsfehler (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Verschlรผsseltes Event (Unbekannter Eventtyp) -- - - - - -- Replay attack! This message index was reused! -- - -- Replay-angriff! Der Nachrichtenindex wurde wiederverwendet! -- - - - - -- Message by unverified device! -- - -- Nachricht von einem unverifizierten Gerรคt! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Nachricht verschlรผsselt bei einem unverifizierten Gerรคt oder der Schlรผssel ist aus einer nicht vertrauenswรผrdigen Quelle wie der Onlineschlรผsselsicherung. @@ -581,17 +581,26 @@ - Device verification timed out. Verifizierung abgelaufen, die andere Seite antwortet nicht. - + Other party canceled the verification. Die andere Seite hat die Verifizierung abgebrochen. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close SchlieรŸen @@ -604,48 +613,138 @@ Nachricht weiterleiten + + ImagePackEditorDialog + + + Editing image pack + Bilderpackung bearbeiten + + + + Add images + Bilder hinzufรผgen + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Sticker (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + Eindeutiger Name + + + + Packname + Paketname + + + + Attribution + Attribution + + + + + Use as Emoji + Als Emoji verwenden + + + + + Use as Sticker + Als Sticker verwenden + + + + Shortcode + Abkรผrzung + + + + Body + Beschreibung + + + + Remove from pack + Vom Paket entfernen + + + + Remove + Entfernen + + + + Cancel + Abbrechen + + + + Save + Spechern + + ImagePackSettingsDialog Image pack settings - + Bilderpackungseinstellungen - + + Create account pack + Neue Packung erstellen + + + + New room pack + Neue Packung + + + Private pack - + Private Packung Pack from this room - + Packung aus diesem Raum Globally enabled pack - + Global aktivierte Packung - + Enable globally - + Global aktivieren Enables this pack to be used in all rooms - + Macht diese Packung in allen Rรคumen verfรผgbar - + + Edit + Bearbeiten + + + Close - SchlieรŸen + SchlieรŸen InputBar - + Select a file Datei auswรคhlen @@ -655,7 +754,7 @@ Alle Dateien (*) - + Failed to upload media. Please try again. Medienupload fehlgeschlagen. Bitte versuche es erneut. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 Lade Benutzer in %1 ein @@ -694,6 +793,32 @@ Abbrechen + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + Raum-ID oder -Alias + + + + LeaveRoomDialog + + + Leave room + Raum verlassen + + + + Are you sure you want to leave? + Willst du wirklich den Raum verlassen? + + LoginPage @@ -760,25 +885,25 @@ Beispiel: https://mein.server:8787 ANMELDEN - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org Du hast eine invalide Matrix ID eingegeben. Normalerwise sehen die so aus: @joe:matrix.org - + Autodiscovery failed. Received malformed response. Automatische Erkennung fehlgeschlagen. Antwort war fehlerhaft. - + Autodiscovery failed. Unknown error when requesting .well-known. Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Benรถtigte Ansprechpunkte nicht auffindbar. Mรถglicherweise kein Matrixserver. @@ -788,30 +913,48 @@ Beispiel: https://mein.server:8787 Erhaltene Antwort war fehlerhaft. Bitte Homeserverdomain prรผfen. - + An unknown error occured. Make sure the homeserver domain is valid. Ein unbekannter Fehler ist aufgetreten. Bitte Homeserverdomain prรผfen. - + SSO LOGIN SSO ANMELDUNG - + Empty password Leeres Passwort - + SSO login failed SSO Anmeldung fehlgeschlagen + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed entfernt @@ -822,7 +965,7 @@ Beispiel: https://mein.server:8787 Verschlรผsselung aktiviert - + room name changed to: %1 Raumname wurde gรคndert auf: %1 @@ -881,6 +1024,11 @@ Beispiel: https://mein.server:8787 Negotiating call... Wรคhltโ€ฆ + + + Allow them in + Reinlassen + MessageInput @@ -905,7 +1053,7 @@ Beispiel: https://mein.server:8787 Schreibe eine Nachrichtโ€ฆ - + Stickers Sticker @@ -928,7 +1076,7 @@ Beispiel: https://mein.server:8787 MessageView - + Edit Bearbeiten @@ -948,17 +1096,19 @@ Beispiel: https://mein.server:8787 Optionen - + + &Copy &Kopieren - + + Copy &link location Kopiere &Link - + Re&act Re&agieren @@ -1017,6 +1167,11 @@ Beispiel: https://mein.server:8787 Copy link to eve&nt Link &zu diesem Event kopieren + + + &Go to quoted message + &Gehe zur zitierten Nachricht + NewVerificationRequest @@ -1031,7 +1186,12 @@ Beispiel: https://mein.server:8787 Verifizierungsanfrage erhalten - + + 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.) + + + + 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? Damit andere Nutzer sehen, welche Gerรคte tatsรคchlich dir gehรถren, kannst du sie verifizieren. Das erlaubt auch Schlรผsselbackup zu nutzen ohne ein Passwort einzugeben. %1 jetzt verifizieren? @@ -1076,33 +1236,29 @@ Beispiel: https://mein.server:8787 Akzeptieren + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 hat eine verschlรผsselte Nachricht gesendet - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - * %1 %2 - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message %1 hat geantwortet: %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,7 +1289,7 @@ Beispiel: https://mein.server:8787 Kein Mikrofon gefunden. - + Voice Sprache @@ -1164,7 +1320,7 @@ Beispiel: https://mein.server:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. Benutze ein separates profil, wodurch mehrere Accounts und Nhekoinstanzen zur gleichen Zeit verwendet werden kรถnnen. @@ -1179,21 +1335,37 @@ Beispiel: https://mein.server:8787 Profilname + + ReadReceipts + + + Read receipts + Lesebestรคtigungen + + + + ReadReceiptsModel + + + Yesterday, %1 + Gestern, %1 + + RegisterPage - + Username Benutzername - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. Der Benutzername sollte nicht leer sein und nur aus a-z, 0-9, ., _, =, - und / bestehen. - + Password Passwort @@ -1213,7 +1385,7 @@ Beispiel: https://mein.server:8787 Heimserver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. Ein Server, der Registrierungen zulรคsst. Weil Matrix ein dezentralisiertes Protokoll ist, musst du erst einen Server ausfindig machen oder einen persรถnlichen Server aufsetzen. @@ -1223,27 +1395,17 @@ Beispiel: https://mein.server:8787 REGISTRIEREN - - No supported registration flows! - Keine unterstรผtzten Registrierungsmethoden! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - Mindestens ein Feld hat invalide Werte. Bitte behebe diese Fehler und versuche es erneut. - - - + Autodiscovery failed. Received malformed response. Automatische Erkennung fehlgeschlagen. Antwort war fehlerhaft. - + Autodiscovery failed. Unknown error when requesting .well-known. Automatische Erkennung fehlgeschlagen. Unbekannter Fehler bei Anfrage .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Benรถtigte Ansprechpunkte nicht auffindbar. Mรถglicherweise kein Matrixserver. @@ -1258,17 +1420,17 @@ Beispiel: https://mein.server:8787 Ein unbekannter Fehler ist aufgetreten. Bitte Homeserverdomain prรผfen. - + Password is not long enough (min 8 chars) Passwort nicht lang genug (mind. 8 Zeichen) - + Passwords don't match Passwรถrter stimmen nicht รผberein - + Invalid server name Ungรผltiger Servername @@ -1276,7 +1438,7 @@ Beispiel: https://mein.server:8787 ReplyPopup - + Close SchlieรŸen @@ -1286,10 +1448,28 @@ Beispiel: https://mein.server:8787 Bearbeiten abbrechen + + RoomDirectory + + + Explore Public Rooms + ร–ffentliche Rรคume erkunden + + + + Search for public rooms + Suche nach รถffentlichen Rรคumen + + + + Choose custom homeserver + + + RoomInfo - + no version stored keine Version gespeichert @@ -1297,7 +1477,7 @@ Beispiel: https://mein.server:8787 RoomList - + New tag Neuer Tag @@ -1306,16 +1486,6 @@ Beispiel: https://mein.server:8787 Enter the tag you want to use: Gib den Tag, den du verwenden willst, ein: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1347,7 +1517,7 @@ Beispiel: https://mein.server:8787 Neuen Tag erstellen... - + Status Message Statusnachricht @@ -1367,12 +1537,35 @@ Beispiel: https://mein.server:8787 Setze eine Statusnachricht - + Logout Abmelden - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + SchlieรŸen + + + Start a new chat Neues Gesprรคch beginnen @@ -1392,7 +1585,7 @@ Beispiel: https://mein.server:8787 Raumverzeichnis - + User settings Benutzereinstellungen @@ -1400,12 +1593,12 @@ Beispiel: https://mein.server:8787 RoomMembers - + Members of %1 Teilnehmer in %1 - + %n people in %1 Summary above list of members @@ -1418,16 +1611,36 @@ Beispiel: https://mein.server:8787 Invite more people Lade mehr Leute ein + + + This room is not encrypted! + Dieser Raum ist nicht verschlรผsselt! + + + + This user is verified. + Der Nutzer ist verifiziert. + + + + This user isn't verified, but is still using the same master key from the first time you met. + Der Nutzer ist nicht verifiziert, aber hat schon immer diese Identitรคt verwendet. + + + + This user has unverified devices! + Dieser Nutzer hat unverifizierte Gerรคte! + RoomSettings - + Room Settings Raumeinstellungen - + %1 member(s) %1 Teilnehmer @@ -1457,7 +1670,12 @@ Beispiel: https://mein.server:8787 Alle Nachrichten - + + Room access + Zugangsberechtigungen + + + Anyone and guests Jeder (inkl. Gรคste) @@ -1472,7 +1690,17 @@ Beispiel: https://mein.server:8787 Eingeladene Nutzer - + + By knocking + Durch Anklopfen + + + + Restricted by membership in other rooms + Durch Teilnahme an anderen Rรคumen + + + Encryption Verschlรผsselung @@ -1490,17 +1718,17 @@ Beispiel: https://mein.server:8787 Sticker & Emote Settings - + Sticker- & Emoteeinstellungen Change - + ร„ndern Change what packs are enabled, remove packs or create new ones - + ร„ndere welche Packungen aktiviert sind, entferne oder erstelle neue Packungen @@ -1518,12 +1746,12 @@ Beispiel: https://mein.server:8787 Raumversion - + Failed to enable encryption: %1 Aktivierung der Verschlรผsselung fehlgeschlagen: %1 - + Select an avatar Wรคhle einen Avatar @@ -1543,8 +1771,8 @@ Beispiel: https://mein.server:8787 Fehler beim Lesen der Datei: %1 - - + + Failed to upload image: %s Hochladen des Bildes fehlgeschlagen: %s @@ -1552,21 +1780,49 @@ Beispiel: https://mein.server:8787 RoomlistModel - + Pending invite. Offene Einladung. - + Previewing this room Vorschau dieses Raums - + No preview available Keine Vorschau verfรผgbar + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1621,6 +1877,121 @@ Beispiel: https://mein.server:8787 Abbrechen + + SecretStorage + + + Failed to connect to secret storage + Verbindung zum kryptografischen Speicher fehlgeschlagen + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Konnte die Bilderpackung nicht aktualisieren: %1 + + + + Failed to delete old image pack: %1 + Konnte die alte Bilderpackung nicht lรถschen: %1 + + + + Failed to open image: %1 + Konnte Bild nicht รถffnen: %1 + + + + Failed to upload image: %1 + Konnte Bild nicht hochladen: %1 + + StatusIndicator @@ -1673,18 +2044,18 @@ Beispiel: https://mein.server:8787 TimelineModel - + Message redaction failed: %1 Nachricht zurรผckziehen fehlgeschlagen: %1 - + Failed to encrypt event, sending aborted! Event konnte nicht verschlรผsselt werden, senden wurde abgebrochen! - + Save image Bild speichern @@ -1704,7 +2075,7 @@ Beispiel: https://mein.server:8787 Datei speichern - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1713,7 +2084,7 @@ Beispiel: https://mein.server:8787 - + %1 opened the room to the public. %1 hat diesen Raum รถffentlich gemacht. @@ -1723,7 +2094,17 @@ Beispiel: https://mein.server:8787 %1 hat eingestellt, dass dieser Raum eine Einladung benรถtigt um beizutreten. - + + %1 allowed to join this room by knocking. + %1 hat erlaubt Leuten diesen Raum durch Anklopfen beizutreten. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 hat erlaubt Mitglieder aus folgenden Rรคumen diesen Raum automatisch zu betreten: %2 + + + %1 made the room open to guests. %1 hat Gรคsten erlaubt den Raum zu betreten. @@ -1743,12 +2124,12 @@ Beispiel: https://mein.server:8787 %1 hat eingestellt, dass nur Teilnehmer Nachrichten in diesem Raum lesen kรถnnen (ab diesem Punkt). - + %1 set the room history visible to members since they were invited. %1 hat eingestellt, dass Teilnehmer die Historie dieses Raums lesen kรถnnen ab dem Zeitpunkt, zu dem sie eingeladen wurden. - + %1 set the room history visible to members since they joined the room. %1 hat eingestellt, dass Teilnehmer die Historie dieses Raums lesen kรถnnen ab dem Zeitpunkt, zu dem sie beigetreten sind. @@ -1758,12 +2139,12 @@ Beispiel: https://mein.server:8787 %1 hat die Berechtigungen dieses Raums bearbeitet. - + %1 was invited. %1 wurde eingeladen. - + %1 changed their avatar. %1 hat den Avatar geรคndert. @@ -1773,12 +2154,17 @@ Beispiel: https://mein.server:8787 %1 hat etwas im Profil geรคndert. - + %1 joined. %1 hat den Raum betreten. - + + %1 joined via authorisation from %2's server. + %1 hat den Raum durch Authorisierung von %2s Server betreten. + + + %1 rejected their invite. %1 hat die Einladung abgewiesen. @@ -1808,32 +2194,32 @@ Beispiel: https://mein.server:8787 %1 wurde gebannt. - + Reason: %1 Grund: %1 - + %1 redacted their knock. %1 hat das Anklopfen zurรผckgezogen. - + You joined this room. Du bist dem Raum beigetreten. - + %1 has changed their avatar and changed their display name to %2. %1 hat den eigenen Avatar und Namen geรคndert zu %2. - + %1 has changed their display name to %2. %1 hat den eigenen Namen geรคndert zu %2. - + Rejected the knock from %1. Hat das Anklopfen von %1 abgewiesen. @@ -1852,7 +2238,7 @@ Beispiel: https://mein.server:8787 TimelineRow - + Edited Bearbeitet @@ -1860,12 +2246,17 @@ Beispiel: https://mein.server:8787 TimelineView - + No room open Kein Raum geรถffnet - + + No preview available + Keine Vorschau verfรผgbar + + + %1 member(s) %1 Teilnehmer @@ -1890,28 +2281,40 @@ Beispiel: https://mein.server:8787 Zurรผck zur Raumliste - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - Keinen verschlรผsselten Chat mit diesem User gefunden. Erstelle einen verschlรผsselten 1:1 Chat mit diesem Nutzer und versuche es erneut. - - TopBar - + Back to room list Zurรผck zur Raumliste - + No room selected Kein Raum ausgewรคhlt - + + This room is not encrypted! + Dieser Raum ist nicht verschlรผsselt! + + + + This room contains only verified devices. + Dieser Raum enthรคlt nur verifizierte Gerรคte. + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + Dieser Raum enthรคlt unverifizierte Gerรคte! + + + Room options Raumoptionen @@ -1949,10 +2352,35 @@ Beispiel: https://mein.server:8787 SchlieรŸen + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Bitte gebe ein gรผltiges Registrierungstoken ein. + + + + Invalid token + + + UserProfile - + Global User Profile Globales Nutzerprofil @@ -1962,33 +2390,98 @@ Beispiel: https://mein.server:8787 Raumspezifisches Nutzerprofil - - + + Change avatar globally. + ร„ndere das Profilbild in allen Rรคumen. + + + + Change avatar. Will only apply to this room. + ร„ndere das Profilbild nur in diesem Raum. + + + + Change display name globally. + ร„ndere den Anzeigenamen in allen Rรคumen. + + + + Change display name. Will only apply to this room. + ร„ndere den Anzeigenamen nur in diesem Raum. + + + + Room: %1 + Raum: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + Dies ist das raumspezifische Nutzerprofil. Der Anzeigename und das Profilbild kann sich von dem globalen Profil unterscheiden. + + + + Open the global profile for this user. + ร–ffne das globale Profil des Nutzers. + + + + Verify Verifizieren - - Ban the user - Banne den Nutzer - - - - Start a private chat - Starte eine private Konservation + + Start a private chat. + Starte eine private Unterhaltung. - Kick the user - Kicke den Nutzer + Kick the user. + Benutzer aus dem Raum werfen. - + + Ban the user. + Benutzer aus dem Raum verbannen. + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify Verifizierung zurรผckziehen - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar Avatar wรคhlen @@ -2011,8 +2504,8 @@ Beispiel: https://mein.server:8787 UserSettings - - + + Default Standard @@ -2020,7 +2513,7 @@ Beispiel: https://mein.server:8787 UserSettingsPage - + Minimize to tray Ins Benachrichtigungsfeld minimieren @@ -2030,22 +2523,22 @@ Beispiel: https://mein.server:8787 Im Benachrichtigungsfeld starten - + Group's sidebar Gruppen-Seitenleiste - + Circular Avatars Runde Profilbilder - + profile: %1 Profil: %1 - + Default Standard @@ -2070,7 +2563,7 @@ Beispiel: https://mein.server:8787 DOWNLOADEN - + Keep the application running in the background after closing the client window. Applikation im Hintergrund weiterlaufen lassen. @@ -2086,6 +2579,16 @@ OFF - square, ON - Circle. ร„ndert das Aussehen von Benutzeravataren. AUS - Quadratisch, AN - Kreis. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2115,7 +2618,7 @@ be blurred. Die Zeitliste wird unscharf, wenn das Fenster den Fokus verliert. - + Privacy screen timeout (in seconds [0 - 3600]) Sichtschutz-Zeitbegrenzung (in Sekunden [0 - 3600]) @@ -2175,7 +2678,7 @@ Wenn das aus ist, werden die Rรคume in der Raumliste rein nach dem Sendezeitpunk Wenn das eingeschaltet ist, werden Nachrichten mit aktiven Erwรคhnung zuerst sortiert (der rote Kreis). Danach kommen andere Benachrichtigungen (weiรŸer Kreis) und zuletzt stummgeschaltete Rรคume sortiert nach deren Zeitstempel. - + Read receipts Lesebestรคtigungen @@ -2187,7 +2690,7 @@ Status is displayed next to timestamps. Der Status wird neben der Nachricht angezeigt. - + Send messages as Markdown Sende Nachrichten als Markdown formatiert @@ -2200,6 +2703,11 @@ Wenn deaktiviert werden alle Nachrichten als unformatierter Text gesendet. + Play animated images only on hover + Animiete Bilder nur abspielen, wenn die Maus รผber diesen ist + + + Desktop notifications Desktopbenachrichtigungen @@ -2241,12 +2749,47 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.Erhรถht die SchriftgrรถรŸe, wenn die Nachricht nur aus ein paar Emoji besteht. - + + Send encrypted messages to verified users only + Sende verschlรผsselte Nachrichten nur an verifizierte Nutzer + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Sendet Schlรผssel fรผr verschlรผsselte Nachrichten nur an verifizierte Gerรคte. Das erhรถht die Sicherheit, aber macht die Ende-zu-Ende Verschlรผsselung komplizierter, weil jeder Nutzer verifiziert werden muss. + + + Share keys with verified users and devices Teile Schlรผssel mit verifizierten Nutzern und Gerรคten - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Automatisch Schlรผssel an verifizierte Nutzer weiterleiten, auch wenn der Nutzer eigentlich keinen Zugriff auf diese Schlรผssel haben sollte. + + + + Online Key Backup + Onlinenachrichtenschlรผsselspeicher + + + + Download message encryption keys from and upload to the encrypted online key backup. + Speichere eine Kopie der Nachrichtenschlรผssel verschlรผsselt auf dem Server. + + + + Enable online key backup + Onlinenachrichtenschlรผsselspeicher aktivieren + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + Die Nhekoentwickler empfehlen aktuell nicht die Onlinesicherung zu ativieren bevor die symmetrische Methode zu verfรผgung steht. Trotzdem aktivieren? + + + CACHED IM CACHE @@ -2256,7 +2799,7 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.NICHT IM CACHE - + Scale factor Skalierungsfaktor @@ -2331,7 +2874,7 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.Gerรคtefingerabdruck - + Session Keys Sitzungsschlรผssel @@ -2351,17 +2894,22 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.VERSCHLรœSSELUNG - + GENERAL ALLGEMEINES - + INTERFACE OBERFLร„CHE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Spiele Medien wie GIF oder WEBP nur ab, wenn du die Maus darรผber bewegst. + + + Touchscreen mode Touchscreenmodus @@ -2376,12 +2924,7 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.Emojischriftart - - Automatically replies to key requests from other users, if they are verified. - Antortet automatsch auf Schlรผsselanfragen von anderen Nutzern, wenn du diese verifiziert hast. - - - + Master signing key Masterverifizierungsschlรผssel @@ -2401,7 +2944,7 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.Der Schlรผssel um andere Nutzer zu verifizieren. Wenn der lokal zwischengespeichert ist, dann werden durch eine Nutzerverifizierung alle Gerรคte verifiziert. - + Self signing key Selbstverifizierungsschlรผssel @@ -2431,14 +2974,14 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.Alle Dateien (*) - + Open Sessions File ร–ffne Sessions Datei - + @@ -2446,19 +2989,19 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.Feher - - + + File Password Password fรผr Datei - + Enter the passphrase to decrypt the file: Bitte gib das Passwort zum Enschlรผsseln der Datei ein: - + The password cannot be empty Das Passwort darf nicht leer sein @@ -2473,6 +3016,14 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.Datei zum Speichern der zu exportierenden Sitzungsschlรผssel + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Keinen verschlรผsselten Chat mit diesem User gefunden. Erstelle einen verschlรผsselten 1:1 Chat mit diesem Nutzer und versuche es erneut. + + Waiting @@ -2598,37 +3149,6 @@ Normalerweise animiert das den Taskbaricon oder fรคrbt das Fenster orange ein.ร–ffne das Fallback, folge den Anweisungen und bestรคtige nach Abschluss via "Bestรคtigen". - - dialogs::JoinRoom - - - Join - Betreten - - - - Cancel - Abbrechen - - - - Room ID or alias - Raum-ID oder -Alias - - - - dialogs::LeaveRoom - - - Cancel - Abbrechen - - - - Are you sure you want to leave? - Willst du wirklich den Raum verlassen? - - dialogs::Logout @@ -2682,32 +3202,6 @@ Medien-GrรถรŸe: %2 Lรถse das reCAPTCHA und drรผcke den "Bestรคtigen"-Knopf - - dialogs::ReadReceipts - - - Read receipts - Lesebestรคtigungen - - - - Close - SchlieรŸen - - - - dialogs::ReceiptItem - - - Today %1 - Heute %1 - - - - Yesterday %1 - Gestern %1 - - message-description sent: @@ -2721,47 +3215,47 @@ Medien-GrรถรŸe: %2 %1 hat eine Audiodatei gesendet - + You sent an image Du hast ein Bild gesendet - + %1 sent an image %1 hat ein Bild gesendet - + You sent a file Du hast eine Datei gesendet - + %1 sent a file %1 hat eine Datei gesendet - + You sent a video Du hast ein Video gesendet - + %1 sent a video %1 hat ein Video gesendet - + You sent a sticker Du hast einen Sticker gesendet - + %1 sent a sticker %1 hat einen Sticker gesendet - + You sent a notification Du hast eine Benachrichtigung gesendet @@ -2776,7 +3270,7 @@ Medien-GrรถรŸe: %2 Du: %1 - + %1: %2 %1: %2 @@ -2796,27 +3290,27 @@ Medien-GrรถรŸe: %2 Du hast angerufen - + %1 placed a call %1 hat angerufen - + You answered a call Du hast einen Anruf angenommen - + %1 answered a call %1 hat einen Anruf angenommen - + You ended a call Du hast einen Anruf beendet - + %1 ended a call %1 hat einen Anruf beendet @@ -2824,7 +3318,7 @@ Medien-GrรถรŸe: %2 utils - + Unknown Message Type Unbekannter Nachrichtentyp diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index d40a6433..88503e86 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... @@ -56,7 +56,7 @@ CallInvite - + Video Call @@ -74,7 +74,7 @@ CallInviteBar - + Video Call @@ -117,7 +117,7 @@ CallManager - + Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. - + Confirm invite - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 @@ -182,7 +182,7 @@ - + Kicked user: %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -247,33 +247,35 @@ - + Failed to restore OLM account. Please login again. + + Failed to restore save data. Please login again. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - - + + Please try to login again: %1 - + Failed to join room: %1 - + You joined the room @@ -283,7 +285,7 @@ - + Room creation failed: %1 @@ -293,7 +295,7 @@ - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + ฮ†ฮบฯ…ฯฮฟ + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close @@ -645,7 +744,7 @@ InputBar - + Select a file ฮ”ฮนฮฌฮปฮตฮพฮต ฮญฮฝฮฑ ฮฑฯฯ‡ฮตฮฏฮฟ @@ -655,7 +754,7 @@ ฮŒฮปฮฑ ฯ„ฮฑ ฮฑฯฯ‡ฮตฮฏฮฑ (*) - + Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ ฮ†ฮบฯ…ฯฮฟ + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + ID ฮฎ ฯŒฮฝฮฟฮผฮฑ ฯƒฯ…ฮฝฮฟฮผฮนฮปฮฏฮฑฯ‚ + + + + LeaveRoomDialog + + + Leave room + ฮ’ฮณฮญฯ‚ + + + + Are you sure you want to leave? + ฮ•ฮฏฯƒฯ„ฮต ฯƒฮฏฮณฮฟฯ…ฯฮฟฮน ฮฟฯ„ฮน ฮธฮญฮปฮตฯ„ฮต ฮฝฮฑ ฮบฮปฮตฮฏฯƒฮตฯ„ฮต ฯ„ฮท ฯƒฯ…ฮฝฮฟฮผฮนฮปฮฏฮฑ; + + LoginPage @@ -756,25 +881,25 @@ Example: https://server.my:8787 ฮ•ฮ™ฮฃฮŸฮ”ฮŸฮฃ - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -784,30 +909,48 @@ Example: https://server.my:8787 - + An unknown error occured. Make sure the homeserver domain is valid. - + SSO LOGIN - + Empty password ฮšฮตฮฝฯŒฯ‚ ฮบฯ‰ฮดฮนฮบฯŒฯ‚ - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed @@ -818,7 +961,7 @@ Example: https://server.my:8787 - + room name changed to: %1 @@ -877,6 +1020,11 @@ Example: https://server.my:8787 Negotiating call... + + + Allow them in + + MessageInput @@ -901,7 +1049,7 @@ Example: https://server.my:8787 ฮ“ฯฮฌฯˆฮต ฮญฮฝฮฑ ฮผฮฎฮฝฯ…ฮผฮฑ... - + Stickers @@ -924,7 +1072,7 @@ Example: https://server.my:8787 MessageView - + Edit @@ -944,17 +1092,19 @@ Example: https://server.my:8787 - + + &Copy - + + Copy &link location - + Re&act @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1072,33 +1232,29 @@ Example: https://server.my:8787 ฮ‘ฯ€ฮฟฮดฮฟฯ‡ฮฎ + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username ฮŒฮฝฮฟฮผฮฑ ฯ‡ฯฮฎฯƒฯ„ฮท - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password ฮšฯ‰ฮดฮนฮบฯŒฯ‚ @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 ฮ•ฮ“ฮ“ฮกฮ‘ฮฆฮ— - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 - + Password is not long enough (min 8 chars) ฮŸ ฮบฯ‰ฮดฮนฮบฯŒฯ‚ ฮดฮตฮฝ ฮฑฯ€ฮฟฯ„ฮตฮปฮตฮฏฯ„ฮฑฮน ฮฑฯ€ฯŒ ฮฑฯฮบฮตฯ„ฮฟฯ…ฯ‚ ฯ‡ฮฑฯฮฑฮบฯ„ฮฎฯฮตฯ‚ - + Passwords don't match ฮŸฮน ฮบฯ‰ฮดฮนฮบฮฟฮฏ ฮดฮตฮฝ ฯ„ฮฑฮนฯฮนฮฏฮฑฮถฮฟฯ…ฮฝ - + Invalid server name ฮ›ฮฑฮฝฮธฮฑฯƒฮผฮญฮฝฮฟ ฯŒฮฝฮฟฮผฮฑ ฮดฮนฮฑฮบฮฟฮผฮนฯƒฯ„ฮฎ @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + + + + Start a new chat @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 - + User settings @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1414,16 +1607,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1453,7 +1666,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1468,7 +1686,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1514,12 +1742,12 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 - + Select an avatar @@ -1539,8 +1767,8 @@ Example: https://server.my:8787 - - + + Failed to upload image: %s @@ -1548,21 +1776,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1617,6 +1873,121 @@ Example: https://server.my:8787 ฮ†ฮบฯ…ฯฮฟ + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1669,18 +2040,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 - + Failed to encrypt event, sending aborted! - + Save image ฮ‘ฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮท ฮ•ฮนฮบฯŒฮฝฮฑฯ‚ @@ -1700,7 +2071,7 @@ Example: https://server.my:8787 - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1709,7 +2080,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1719,7 +2090,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1739,12 +2120,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1754,12 +2135,12 @@ Example: https://server.my:8787 - + %1 was invited. - + %1 changed their avatar. @@ -1769,12 +2150,17 @@ Example: https://server.my:8787 - + %1 joined. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. @@ -1804,32 +2190,32 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. @@ -1848,7 +2234,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1856,12 +2242,17 @@ Example: https://server.my:8787 TimelineView - + No room open - + + No preview available + + + + %1 member(s) @@ -1886,28 +2277,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options @@ -1945,10 +2348,35 @@ Example: https://server.my:8787 ฮˆฮพฮฟฮดฮฟฯ‚ + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1958,33 +2386,98 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar @@ -2007,8 +2500,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2016,7 +2509,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray ฮ•ฮปฮฑฯ‡ฮนฯƒฯ„ฮฟฯ€ฮฟฮฏฮทฯƒฮท @@ -2026,22 +2519,22 @@ Example: https://server.my:8787 - + Group's sidebar - + Circular Avatars - + profile: %1 - + Default @@ -2066,7 +2559,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2081,6 +2574,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2109,7 +2612,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts @@ -2175,7 +2678,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown @@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications @@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL ฮ“ฮ•ฮฮ™ฮšฮ‘ - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash ฮŒฮปฮฑ ฯ„ฮฑ ฮฑฯฯ‡ฮตฮฏฮฑ (*) - + Open Sessions File - + @@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash - - + + File Password - + Enter the passphrase to decrypt the file: - + The password cannot be empty @@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - ฮ†ฮบฯ…ฯฮฟ - - - - Room ID or alias - ID ฮฎ ฯŒฮฝฮฟฮผฮฑ ฯƒฯ…ฮฝฮฟฮผฮนฮปฮฏฮฑฯ‚ - - - - dialogs::LeaveRoom - - - Cancel - ฮ†ฮบฯ…ฯฮฟ - - - - Are you sure you want to leave? - ฮ•ฮฏฯƒฯ„ฮต ฯƒฮฏฮณฮฟฯ…ฯฮฟฮน ฮฟฯ„ฮน ฮธฮญฮปฮตฯ„ฮต ฮฝฮฑ ฮบฮปฮตฮฏฯƒฮตฯ„ฮต ฯ„ฮท ฯƒฯ…ฮฝฮฟฮผฮนฮปฮฏฮฑ; - - dialogs::Logout @@ -2666,32 +3186,6 @@ Media size: %2 - - dialogs::ReadReceipts - - - Read receipts - - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2705,47 +3199,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2760,7 +3254,7 @@ Media size: %2 - + %1: %2 @@ -2780,27 +3274,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2808,7 +3302,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index 1851fff1..5768e051 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Calling... @@ -56,7 +56,7 @@ CallInvite - + Video Call Video Call @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Video Call @@ -117,7 +117,7 @@ CallManager - + Entire screen Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 Failed to invite user: %1 - + Invited user: %1 Invited user: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Confirm join Confirm join @@ -151,23 +151,23 @@ Do you really want to join %1? - + Room %1 created. Room %1 created. - + Confirm invite Confirm invite - + Do you really want to invite %1 (%2)? Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 Failed to invite %1 to %2: %3 @@ -182,7 +182,7 @@ Do you really want to kick %1 (%2)? - + Kicked user: %1 Kicked user: %1 @@ -197,7 +197,7 @@ Do you really want to ban %1 (%2)? - + Failed to ban %1 in %2: %3 Failed to ban %1 in %2: %3 @@ -217,7 +217,7 @@ Do you really want to unban %1 (%2)? - + Failed to unban %1 in %2: %3 Failed to unban %1 in %2: %3 @@ -227,12 +227,12 @@ Unbanned user: %1 - + Do you really want to start a private chat with %1? Do you really want to start a private chat with %1? - + Cache migration failed! Cache migration failed! @@ -247,33 +247,35 @@ The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache. - + Failed to restore OLM account. Please login again. Failed to restore OLM account. Please login again. + + Failed to restore save data. Please login again. Failed to restore save data. Please login again. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Failed to setup encryption keys. Server response: %1 %2. Please try again later. - - + + Please try to login again: %1 Please try to login again: %1 - + Failed to join room: %1 Failed to join room: %1 - + You joined the room You joined the room @@ -283,7 +285,7 @@ Failed to remove invite: %1 - + Room creation failed: %1 Room creation failed: %1 @@ -293,7 +295,7 @@ Failed to leave room: %1 - + Failed to kick %1 from %2: %3 Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets Decrypt secrets @@ -362,12 +364,12 @@ Enter your recovery key or passphrase to decrypt your secrets: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed Decryption failed @@ -431,7 +433,7 @@ Search - + People People @@ -494,6 +496,49 @@ They match! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + There was an internal error reading the decryption key from the database. + There was an internal error reading the decryption key from the database. + + + + There was an error decrypting this message. + There was an error decrypting this message. + + + + The message couldn't be parsed. + The message couldn't be parsed. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + Unknown decryption error + Unknown decryption error + + + + Request key + Request key + + EncryptionIndicator @@ -513,53 +558,8 @@ - Encrypted by an unverified device - Encrypted by an unverified device - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Encrypted Event (No keys found for decryption) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - -- Encrypted Event (Key not valid for this index) -- - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- Decryption Error (failed to retrieve megolm keys from db) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Decryption Error (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Encrypted Event (Unknown event type) -- - - - - -- Replay attack! This message index was reused! -- - -- Replay attack! This message index was reused! -- - - - - -- Message by unverified device! -- - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. Device verification timed out. - + Other party canceled the verification. Other party canceled the verification. - + + Verification messages received out of order! + Verification messages received out of order! + + + + Unknown verification error. + Unknown verification error. + + + Close Close @@ -604,48 +613,138 @@ Forward Message + + ImagePackEditorDialog + + + Editing image pack + Editing image pack + + + + Add images + Add images + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + State key + + + + Packname + Packname + + + + Attribution + Attribution + + + + + Use as Emoji + Use as Emoji + + + + + Use as Sticker + Use as Sticker + + + + Shortcode + Shortcode + + + + Body + Body + + + + Remove from pack + Remove from pack + + + + Remove + Remove + + + + Cancel + Cancel + + + + Save + Save + + ImagePackSettingsDialog Image pack settings - + Image pack settings - + + Create account pack + Create account pack + + + + New room pack + New room pack + + + Private pack - + Private pack Pack from this room - + Pack from this room Globally enabled pack - + Globally enabled pack - + Enable globally - + Enable globally Enables this pack to be used in all rooms - + Enables this pack to be used in all rooms - + + Edit + Edit + + + Close - Close + Close InputBar - + Select a file Select a file @@ -655,7 +754,7 @@ All Files (*) - + Failed to upload media. Please try again. Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 Invite users to %1 @@ -694,6 +793,32 @@ Cancel + + JoinRoomDialog + + + Join room + Join room + + + + Room ID or alias + Room ID or alias + + + + LeaveRoomDialog + + + Leave room + Leave room + + + + Are you sure you want to leave? + Are you sure you want to leave? + + LoginPage @@ -760,25 +885,25 @@ Example: https://server.my:8787 LOGIN - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. Autodiscovery failed. Unknown error while requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. The required endpoints were not found. Possibly not a Matrix server. @@ -788,35 +913,53 @@ Example: https://server.my:8787 Received malformed response. Make sure the homeserver domain is valid. - + An unknown error occured. Make sure the homeserver domain is valid. An unknown error occured. Make sure the homeserver domain is valid. - + SSO LOGIN SSO LOGIN - + Empty password Empty password - + SSO login failed SSO login failed + + LogoutDialog + + + Log out + Log out + + + + A call is in progress. Log out? + A call is in progress. Log out? + + + + Are you sure you want to log out? + Are you sure you want to log out? + + MessageDelegate - + Encryption enabled Encryption enabled - + room name changed to: %1 room name changed to: %1 @@ -866,18 +1009,23 @@ Example: https://server.my:8787 Negotiating callโ€ฆ - + + Allow them in + Allow them in + + + %1 answered the call. %1 answered the call. - + removed removed - + %1 ended the call. %1 ended the call. @@ -905,7 +1053,7 @@ Example: https://server.my:8787 Write a messageโ€ฆ - + Stickers Stickers @@ -928,7 +1076,7 @@ Example: https://server.my:8787 MessageView - + Edit Edit @@ -948,17 +1096,19 @@ Example: https://server.my:8787 Options - + + &Copy &Copy - + + Copy &link location Copy &link location - + Re&act Re&act @@ -1017,6 +1167,11 @@ Example: https://server.my:8787 Copy link to eve&nt Copy link to eve&nt + + + &Go to quoted message + &Go to quoted message + NewVerificationRequest @@ -1031,7 +1186,12 @@ Example: https://server.my:8787 Received Verification Request - + + 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.) + 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.) + + + 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? 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? @@ -1076,33 +1236,29 @@ Example: https://server.my:8787 Accept + + NotificationWarning + + + You are about to notify the whole room + You are about to notify the whole room + + NotificationsManager - - + + %1 sent an encrypted message %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - * %1 %2 - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message %1 replied: %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,7 +1289,7 @@ Example: https://server.my:8787 No microphone found. - + Voice Voice @@ -1164,7 +1320,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1179,21 +1335,37 @@ Example: https://server.my:8787 profile name + + ReadReceipts + + + Read receipts + Read receipts + + + + ReadReceiptsModel + + + Yesterday, %1 + Yesterday, %1 + + RegisterPage - + Username Username - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password Password @@ -1213,7 +1385,7 @@ Example: https://server.my:8787 Homeserver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1223,27 +1395,17 @@ Example: https://server.my:8787 REGISTER - - No supported registration flows! - No supported registration flows! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - One or more fields have invalid inputs. Please correct those issues and try again. - - - + Autodiscovery failed. Received malformed response. Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. Autodiscovery failed. Unknown error while requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. The required endpoints were not found. Possibly not a Matrix server. @@ -1258,17 +1420,17 @@ Example: https://server.my:8787 An unknown error occured. Make sure the homeserver domain is valid. - + Password is not long enough (min 8 chars) Password is not long enough (min 8 chars) - + Passwords don't match Passwords don't match - + Invalid server name Invalid server name @@ -1276,7 +1438,7 @@ Example: https://server.my:8787 ReplyPopup - + Close Close @@ -1286,10 +1448,28 @@ Example: https://server.my:8787 Cancel edit + + RoomDirectory + + + Explore Public Rooms + Explore Public Rooms + + + + Search for public rooms + Search for public rooms + + + + Choose custom homeserver + Choose custom homeserver + + RoomInfo - + no version stored no version stored @@ -1297,7 +1477,7 @@ Example: https://server.my:8787 RoomList - + New tag New tag @@ -1306,16 +1486,6 @@ Example: https://server.my:8787 Enter the tag you want to use: Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1347,7 +1517,7 @@ Example: https://server.my:8787 Create new tag... - + Status Message Status Message @@ -1367,12 +1537,35 @@ Example: https://server.my:8787 Set status message - + Logout Logout - + + Encryption not set up + Cross-signing setup has not run yet. + Encryption not set up + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + Unverified login + + + + Please verify your other devices + There are unverified devices signed in to this account. + Please verify your other devices + + + + Close + Close + + + Start a new chat Start a new chat @@ -1392,7 +1585,7 @@ Example: https://server.my:8787 Room directory - + User settings User settings @@ -1400,12 +1593,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 Members of %1 - + %n people in %1 Summary above list of members @@ -1418,16 +1611,36 @@ Example: https://server.my:8787 Invite more people Invite more people + + + This room is not encrypted! + This room is not encrypted! + + + + This user is verified. + This user is verified. + + + + This user isn't verified, but is still using the same master key from the first time you met. + This user isn't verified, but is still using the same master key from the first time you met. + + + + This user has unverified devices! + This user has unverified devices! + RoomSettings - + Room Settings Room Settings - + %1 member(s) %1 member(s) @@ -1457,7 +1670,12 @@ Example: https://server.my:8787 All messages - + + Room access + Room access + + + Anyone and guests Anyone and guests @@ -1472,7 +1690,17 @@ Example: https://server.my:8787 Invited users - + + By knocking + By knocking + + + + Restricted by membership in other rooms + Restricted by membership in other rooms + + + Encryption Encryption @@ -1490,17 +1718,17 @@ Example: https://server.my:8787 Sticker & Emote Settings - + Sticker & Emote Settings Change - + Change Change what packs are enabled, remove packs or create new ones - + Change what packs are enabled, remove packs or create new ones @@ -1518,12 +1746,12 @@ Example: https://server.my:8787 Room Version - + Failed to enable encryption: %1 Failed to enable encryption: %1 - + Select an avatar Select an avatar @@ -1543,8 +1771,8 @@ Example: https://server.my:8787 Error while reading file: %1 - - + + Failed to upload image: %s Failed to upload image: %s @@ -1552,21 +1780,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. Pending invite. - + Previewing this room Previewing this room - + No preview available No preview available + + Root + + + Please enter your login password to continue: + Please enter your login password to continue: + + + + Please enter a valid email address to continue: + Please enter a valid email address to continue: + + + + Please enter a valid phone number to continue: + Please enter a valid phone number to continue: + + + + Please enter the token, which has been sent to you: + Please enter the token, which has been sent to you: + + + + Wait for the confirmation link to arrive, then continue. + Wait for the confirmation link to arrive, then continue. + + ScreenShare @@ -1621,6 +1877,123 @@ Example: https://server.my:8787 Cancel + + SecretStorage + + + Failed to connect to secret storage + Failed to connect to secret storage + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + SelfVerificationCheck + + + 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! + 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! + + + + Encryption setup successfully + Encryption setup successfully + + + + Failed to setup encryption: %1 + Failed to setup encryption: %1 + + + + Setup Encryption + Setup Encryption + + + + Hello and welcome to Matrix! +It 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! + Hello and welcome to Matrix! +It 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! + + + + Activate Encryption + Activate Encryption + + + + 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. +If 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. + 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. +If 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. + + + + verify + verify + + + + enter passphrase + enter passphrase + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + Failed to create keys for cross-signing! + + + + Failed to create keys for online key backup! + Failed to create keys for online key backup! + + + + Failed to create keys secure server side secret storage! + Failed to create keys secure server side secret storage! + + + + Encryption Setup + Encryption Setup + + + + Encryption setup failed: %1 + Encryption setup failed: %1 + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Failed to update image pack: %1 + + + + Failed to delete old image pack: %1 + Failed to delete old image pack: %1 + + + + Failed to open image: %1 + Failed to open image: %1 + + + + Failed to upload image: %1 + Failed to upload image: %1 + + StatusIndicator @@ -1673,18 +2046,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Message redaction failed: %1 - + Failed to encrypt event, sending aborted! Failed to encrypt event, sending aborted! - + Save image Save image @@ -1704,7 +2077,7 @@ Example: https://server.my:8787 Save file - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1713,7 +2086,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. %1 opened the room to the public. @@ -1723,7 +2096,17 @@ Example: https://server.my:8787 %1 made this room require an invitation to join. - + + %1 allowed to join this room by knocking. + %1 allowed to join this room by knocking. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 allowed members of the following rooms to automatically join this room: %2 + + + %1 made the room open to guests. %1 made the room open to guests. @@ -1743,12 +2126,12 @@ Example: https://server.my:8787 %1 set the room history visible to members from this point on. - + %1 set the room history visible to members since they were invited. %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. %1 set the room history visible to members since they joined the room. @@ -1758,12 +2141,12 @@ Example: https://server.my:8787 %1 has changed the room's permissions. - + %1 was invited. %1 was invited. - + %1 changed their avatar. %1 changed their avatar. @@ -1773,12 +2156,17 @@ Example: https://server.my:8787 %1 changed some profile info. - + %1 joined. %1 joined. - + + %1 joined via authorisation from %2's server. + %1 joined via authorisation from %2's server. + + + %1 rejected their invite. %1 rejected their invite. @@ -1808,32 +2196,32 @@ Example: https://server.my:8787 %1 was banned. - + Reason: %1 Reason: %1 - + %1 redacted their knock. %1 redacted their knock. - + You joined this room. You joined this room. - + %1 has changed their avatar and changed their display name to %2. %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. %1 has changed their display name to %2. - + Rejected the knock from %1. Rejected the knock from %1. @@ -1852,7 +2240,7 @@ Example: https://server.my:8787 TimelineRow - + Edited Edited @@ -1860,12 +2248,17 @@ Example: https://server.my:8787 TimelineView - + No room open No room open - + + No preview available + No preview available + + + %1 member(s) %1 member(s) @@ -1890,28 +2283,40 @@ Example: https://server.my:8787 Back to room list - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - TopBar - + Back to room list Back to room list - + No room selected No room selected - + + This room is not encrypted! + This room is not encrypted! + + + + This room contains only verified devices. + This room contains only verified devices. + + + + This room contains verified devices and devices which have never changed their master key. + This room contains verified devices and devices which have never changed their master key. + + + + This room contains unverified devices! + This room contains unverified devices! + + + Room options Room options @@ -1949,10 +2354,35 @@ Example: https://server.my:8787 Quit + + UIA + + + No available registration flows! + No available registration flows! + + + + + + Registration aborted + Registration aborted + + + + Please enter a valid registration token. + Please enter a valid registration token. + + + + Invalid token + Invalid token + + UserProfile - + Global User Profile Global User Profile @@ -1962,33 +2392,98 @@ Example: https://server.my:8787 Room User Profile - - + + Change avatar globally. + Change avatar globally. + + + + Change avatar. Will only apply to this room. + Change avatar. Will only apply to this room. + + + + Change display name globally. + Change display name globally. + + + + Change display name. Will only apply to this room. + Change display name. Will only apply to this room. + + + + Room: %1 + Room: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + Open the global profile for this user. + Open the global profile for this user. + + + + Verify Verify - - Ban the user - Ban the user - - - - Start a private chat - Start a private chat + + Start a private chat. + Start a private chat. - Kick the user - Kick the user + Kick the user. + Kick the user. - + + Ban the user. + Ban the user. + + + + Refresh device list. + Refresh device list. + + + + Sign out this device. + Sign out this device. + + + + Change device name. + Change device name. + + + + Last seen %1 from %2 + Last seen %1 from %2 + + + Unverify Unverify - + + Sign out device %1 + Sign out device %1 + + + + You signed out this device. + You signed out this device. + + + Select an avatar Select an avatar @@ -2011,8 +2506,8 @@ Example: https://server.my:8787 UserSettings - - + + Default Default @@ -2020,7 +2515,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray Minimize to tray @@ -2030,22 +2525,22 @@ Example: https://server.my:8787 Start in tray - + Group's sidebar Group's sidebar - + Circular Avatars Circular Avatars - + profile: %1 profile: %1 - + Default Default @@ -2070,7 +2565,7 @@ Example: https://server.my:8787 DOWNLOAD - + Keep the application running in the background after closing the client window. Keep the application running in the background after closing the client window. @@ -2086,6 +2581,16 @@ OFF - square, ON - Circle. Change the appearance of user avatars in chats. OFF - square, ON - Circle. + + + Use identicons + Use identicons + + + + Display an identicon instead of a letter when a user has not set an avatar. + Display an identicon instead of a letter when a user has not set an avatar. + Show a column containing groups and tags next to the room list. @@ -2116,7 +2621,7 @@ be blurred. be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) Privacy screen timeout (in seconds [0 - 3600]) @@ -2176,7 +2681,7 @@ If this is off, the list of rooms will only be sorted by the timestamp of the la If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. - + Read receipts Read receipts @@ -2188,7 +2693,7 @@ Status is displayed next to timestamps. Status is displayed next to timestamps. - + Send messages as Markdown Send messages as Markdown @@ -2201,6 +2706,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + Play animated images only on hover + + + Desktop notifications Desktop notifications @@ -2242,12 +2752,47 @@ This usually causes the application icon in the task bar to animate in some fash Make font size larger if messages with only a few emojis are displayed. - + + Send encrypted messages to verified users only + Send encrypted messages to verified users only + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + Share keys with verified users and devices Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + Online Key Backup + Online Key Backup + + + + Download message encryption keys from and upload to the encrypted online key backup. + Download message encryption keys from and upload to the encrypted online key backup. + + + + Enable online key backup + Enable online key backup + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + CACHED CACHED @@ -2257,7 +2802,7 @@ This usually causes the application icon in the task bar to animate in some fash NOT CACHED - + Scale factor Scale factor @@ -2332,7 +2877,7 @@ This usually causes the application icon in the task bar to animate in some fash Device Fingerprint - + Session Keys Session Keys @@ -2352,17 +2897,22 @@ This usually causes the application icon in the task bar to animate in some fash ENCRYPTION - + GENERAL GENERAL - + INTERFACE INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + Touchscreen mode Touchscreen mode @@ -2377,12 +2927,7 @@ This usually causes the application icon in the task bar to animate in some fash Emoji Font Family - - Automatically replies to key requests from other users, if they are verified. - Automatically replies to key requests from other users, if they are verified. - - - + Master signing key Master signing key @@ -2402,7 +2947,7 @@ This usually causes the application icon in the task bar to animate in some fash The key to verify other users. If it is cached, verifying a user will verify all their devices. - + Self signing key Self signing key @@ -2432,14 +2977,14 @@ This usually causes the application icon in the task bar to animate in some fash All Files (*) - + Open Sessions File Open Sessions File - + @@ -2447,19 +2992,19 @@ This usually causes the application icon in the task bar to animate in some fash Error - - + + File Password File Password - + Enter the passphrase to decrypt the file: Enter the passphrase to decrypt the file: - + The password cannot be empty The password cannot be empty @@ -2474,6 +3019,14 @@ This usually causes the application icon in the task bar to animate in some fash File to save the exported session keys + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + Waiting @@ -2599,37 +3152,6 @@ This usually causes the application icon in the task bar to animate in some fash Open the fallback, follow the steps and confirm after completing them. - - dialogs::JoinRoom - - - Join - Join - - - - Cancel - Cancel - - - - Room ID or alias - Room ID or alias - - - - dialogs::LeaveRoom - - - Cancel - Cancel - - - - Are you sure you want to leave? - Are you sure you want to leave? - - dialogs::Logout @@ -2683,32 +3205,6 @@ Media size: %2 Solve the reCAPTCHA and press the confirm button - - dialogs::ReadReceipts - - - Read receipts - Read receipts - - - - Close - Close - - - - dialogs::ReceiptItem - - - Today %1 - Today %1 - - - - Yesterday %1 - Yesterday %1 - - message-description sent: @@ -2722,47 +3218,47 @@ Media size: %2 %1 sent an audio clip - + You sent an image You sent an image - + %1 sent an image %1 sent an image - + You sent a file You sent a file - + %1 sent a file %1 sent a file - + You sent a video You sent a video - + %1 sent a video %1 sent a video - + You sent a sticker You sent a sticker - + %1 sent a sticker %1 sent a sticker - + You sent a notification You sent a notification @@ -2777,7 +3273,7 @@ Media size: %2 You: %1 - + %1: %2 %1: %2 @@ -2797,27 +3293,27 @@ Media size: %2 You placed a call - + %1 placed a call %1 placed a call - + You answered a call You answered a call - + %1 answered a call %1 answered a call - + You ended a call You ended a call - + %1 ended a call %1 ended a call @@ -2825,7 +3321,7 @@ Media size: %2 utils - + Unknown Message Type Unknown Message Type diff --git a/resources/langs/nheko_eo.ts b/resources/langs/nheko_eo.ts index a77c5c25..0d80dbcf 100644 --- a/resources/langs/nheko_eo.ts +++ b/resources/langs/nheko_eo.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Vokanteโ€ฆ @@ -22,7 +22,7 @@ Hide/Show Picture-in-Picture - Kaลi/Montri bildon en bildo + Kaลi/Montri ยซbildon en bildoยป @@ -56,7 +56,7 @@ CallInvite - + Video Call Vidvoko @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Vidvoko @@ -117,31 +117,31 @@ CallManager - + Entire screen - Tuta ekrano + Tuta ekrano ChatPage - + Failed to invite user: %1 Malsukcesis inviti uzanton: %1 - + Invited user: %1 - Invitita uzanto: %1 + Invitita uzanto: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. Malsukcesis migrado de kaลmemoro al nuna versio. Tio povas havi diversajn kialojn. Bonvolu raporti eraron kaj dume provi malpli novan version. Alternative, vi povas provi forigi la kaลmemoron permane. - + Confirm join Konfirmu aliฤon @@ -151,24 +151,24 @@ ฤˆu vi certe volas aliฤi al %1? - + Room %1 created. I believe that the -at ending is correct here. ฤˆambro %1 farit. - + Confirm invite Konfirmu inviton - + Do you really want to invite %1 (%2)? ฤˆu vi certe volas inviti uzanton %1 (%2)? - + Failed to invite %1 to %2: %3 Malsukcesis inviti uzanton %1 al %2: %3 @@ -183,7 +183,7 @@ ฤˆu vi certe volas forpeli uzanton %1 (%2)? - + Kicked user: %1 Forpelis uzanton: %1 @@ -198,7 +198,7 @@ ฤˆu vi certe volas forbari uzanton %1 (%2)? - + Failed to ban %1 in %2: %3 Malsukcesis forbari uzanton %1 en %2: %3 @@ -218,7 +218,7 @@ ฤˆu vi certe volas malforbari uzanton %1 (%2)? - + Failed to unban %1 in %2: %3 Malsukcesis malforbari uzanton %1 en %2: %3 @@ -228,19 +228,19 @@ Malforbaris uzanton: %1 - + Do you really want to start a private chat with %1? ฤˆu vi certe volas komenci privatan babilon kun %1? - + Cache migration failed! Malsukcesis migrado de kaลmemoro! Incompatible cache version - Neakorda versio de kaลmemoro + Neakorda versio de kaลmemoro @@ -248,33 +248,35 @@ La kaลmemoro sur via disko estas pli nova ol kiom ฤ‰i tiu versio de Nheko subtenas. Bonvolu ฤisdatigi la programon aลญ vakigi vian kaลmemoron. - + Failed to restore OLM account. Please login again. - + Malsukcesis rehavi konton je OLM. Bonvolu resaluti. + + Failed to restore save data. Please login again. - + Malsukcesis rehavi konservitajn datumojn. Bonvolu resaluti. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Malsukcesis agordi ฤ‰ifrajn ลlosilojn. Respondo de servilo: %1 %2. Bonvolu reprovi poste. - - + + Please try to login again: %1 Bonvolu provi resaluti: %1 - + Failed to join room: %1 Malsukcesis aliฤi al ฤ‰ambro: %1 - + You joined the room Vi aliฤis la ฤ‰ambron @@ -284,7 +286,7 @@ Malsukcesis forigi inviton: %1 - + Room creation failed: %1 Malsukcesis krei ฤ‰ambron: %1 @@ -294,7 +296,7 @@ Malsukcesis eliri el ฤ‰ambro: %1 - + Failed to kick %1 from %2: %3 Malsukcesis forpeli uzanton %1 de %2: %3 @@ -304,7 +306,7 @@ Hide rooms with this tag or from this space by default. - + Implicite kaลi ฤ‰ambrojn kun ฤ‰i tiu etikedo aลญ de ฤ‰i tiu aro. @@ -312,48 +314,48 @@ All rooms - ฤˆiuj ฤ‰ambroj + ฤˆiuj ฤ‰ambroj Shows all rooms without filtering. - + Montras ฤ‰iujn ฤ‰ambrojn sen filtrado. Favourites - + Elstaraj Rooms you have favourited. - + ฤˆambroj, kiujn vi elstarigis. Low Priority - Malalta prioritato + Malalta prioritato Rooms with low priority. - + ฤˆambroj kun malalta prioritato. Server Notices - + Avizoj de servilo Messages from your server or administrator. - + Mesaฤoj de via servilo aลญ administranto. CrossSigningSecrets - + Decrypt secrets Malฤ‰ifri sekretojn @@ -363,12 +365,12 @@ Enigu vian rehavan ลlosilon aลญ pasfrazon por malฤ‰ifri viajn sekretojn: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Enigu vian rehavan ลlosilon aลญ pasfrazon kun nomo %1 por malฤ‰ifri viajn sekretojn: - + Decryption failed Malsukcesis malฤ‰ifrado @@ -432,7 +434,7 @@ Serฤ‰u - + People Homoj @@ -495,6 +497,49 @@ Ili akordas! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Estas neniu ลloslio por malลlosi ฤ‰i tiun mesaฤon. Ni petis ฤin memage, sed vi povas provi repeti ฤin, se vi rapidas. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Ne povis malฤ‰ifri ฤ‰i tiun mesaฤon, ฤ‰ar ni havas nur ลlosilon por pli novaj. Vi povas provi peti aliron al ฤ‰i tiu mesaฤo. + + + + There was an internal error reading the decryption key from the database. + Eraris interne legado de malฤ‰ifra ลlosilo el la datumbazo. + + + + There was an error decrypting this message. + Eraris malฤ‰ifrado de ฤ‰i tiu mesaฤo. + + + + The message couldn't be parsed. + Ne povis trakti la mesaฤon. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + La ฤ‰ifra ลlosilo estas reuzita! Eble iu provas enmeti falsitajn mesaฤojn en la babilon! + + + + Unknown decryption error + Nekonata malฤ‰ifra eraro + + + + Request key + Peti ลlosilon + + EncryptionIndicator @@ -514,53 +559,8 @@ - Encrypted by an unverified device - ฤˆifrita de nekontrolita aparato - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- - + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + ฤˆifrita de nekontrolita aparato, aลญ per ลlosilo de nefidata fonto, ekzemple la deponejo de ลlosiloj. @@ -582,17 +582,26 @@ - Device verification timed out. Trafiฤis tempolimo de aparata kontrolo. - + Other party canceled the verification. Aliulo nuligis la kontrolon. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close Fermi @@ -602,7 +611,82 @@ Forward Message - + Plusendi mesaฤon + + + + ImagePackEditorDialog + + + Editing image pack + Redaktado de bildopako + + + + Add images + Aldoni bildojn + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Glumarkoj (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + Identigilo (stata ลlosilo) + + + + Packname + Nomo de pako + + + + Attribution + Atribuo + + + + + Use as Emoji + Uzi kiel bildosignon + + + + + Use as Sticker + Uzi kiel glumarkon + + + + Shortcode + Mallongigo + + + + Body + Korpo + + + + Remove from pack + Forigi de pako + + + + Remove + Forigi + + + + Cancel + Nuligi + + + + Save + Konservi @@ -610,45 +694,60 @@ Image pack settings - + Agordoj de bildopako - + + Create account pack + Krei kontan pakon + + + + New room pack + Nova ฤ‰ambra pako + + + Private pack - + Privata pako Pack from this room - + Pakoj el ฤ‰i tiu ฤ‰ambro Globally enabled pack - + ฤˆie ลaltita pako - + Enable globally - + ลœalti ฤ‰ie Enables this pack to be used in all rooms - + ลœaltas ฤ‰i tiun pakon por uzo en ฤ‰iuj ฤ‰ambroj - + + Edit + Redakti + + + Close - Fermi + Fermi InputBar - + Select a file - Elektu dosieron + Elektu dosieron @@ -656,7 +755,7 @@ ฤˆiuj dosieroj (*) - + Failed to upload media. Please try again. Malsukcesis alลuti vidaลญdaฤตojn. Bonvolu reprovi. @@ -664,35 +763,61 @@ InviteDialog - + Invite users to %1 - + Invitu uzantojn al %1 User ID to invite - + Identigilo de invitota uzanto @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @tacuo:matrix.org Add - + Aldoni Invite - + Inviti Cancel - Nuligi + Nuligi + + + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + Identigilo aลญ kromnomo de ฤ‰ambro + + + + LeaveRoomDialog + + + Leave room + Eliri el ฤ‰ambro + + + + Are you sure you want to leave? + ฤˆu vi certas, ke vi volas foriri? @@ -713,7 +838,8 @@ You can also put your homeserver address there, if your server doesn't support .well-known lookup. Example: @user:server.my If Nheko fails to discover your homeserver, it will show you a field to enter the server manually. - Via saluta nomo. Identigilo de Matrikso devus komenciฤi per @ sekvata de la identigilo de uzanto. Post la identigilo, vi devas meti retnomon post :. Vi ankaลญ povas enmeti adreson de via hejmservilo, se via servilo ne subtenas bone-konatan trovmanieron. + Via saluta nomo. Matriksa identigilo devus komenciฤi per @ sekvata de la identigilo de uzanto. Post la identigilo, vi devas meti nomon de via servilo post :. +Vi ankaลญ povas enmeti adreson de via hejmservilo, se via servilo ne subtenas bone-konatan trovmanieron. Ekzemplo: @uzanto:servilo.mia Se Nheko malsukcesas trovi vian hejmservilon, ฤi montros kampon por ฤia permana aldono. @@ -763,58 +889,76 @@ Ekzemplo: https://servilo.mia:8787 SALUTI - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org Vi enigis nevalidan identigilon de Matrikso ekz. @tacuo:matrix.org - + Autodiscovery failed. Received malformed response. - + Malsukcesis memaga trovado. Ricevis misformitan respondon. - + Autodiscovery failed. Unknown error when requesting .well-known. - + Malsukcesis memaga trovado. Okazis nekonata eraro dum petado. .well-known. - + The required endpoints were not found. Possibly not a Matrix server. - + La bezonataj konektaj lokoj ne troviฤis. Eble tio ne estas Matriksa servilo. Received malformed response. Make sure the homeserver domain is valid. - + Ricevis misformitan respondon. Certiฤu, ke retnomo de la hejmservilo estas valida. - + An unknown error occured. Make sure the homeserver domain is valid. - + Okazis nekonata eraro. Certiฤu, ke retnomo de la hejmservilo estas valida. - + SSO LOGIN UNUNURA SALUTO - + Empty password Malplena pasvorto - + SSO login failed Malsukcesis ununura saluto + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed forigita @@ -822,52 +966,52 @@ Ekzemplo: https://servilo.mia:8787 Encryption enabled - + ฤˆifrado estas ลaltita - + room name changed to: %1 Nomo da ฤ‰ambro ลanฤiฤis al: %1 removed room name - + forigis nomon de ฤ‰ambro topic changed to: %1 - + temo ลanฤiฤis al: %1 removed topic - + forigis temon %1 changed the room avatar - + %1 ลanฤis bildon de la ฤ‰ambro %1 created and configured room: %2 - + %1 kreis kaj agordis ฤ‰ambron: %2 %1 placed a voice call. - %1 metis voฤ‰vokon. + %1 voฤ‰vokis. %1 placed a video call. - %1 metis vidvokon. + %1 vidvokis. %1 placed a call. - %1 metis vokon. + %1 vokis. @@ -884,13 +1028,18 @@ Ekzemplo: https://servilo.mia:8787 Negotiating call... Traktante vokonโ€ฆ + + + Allow them in + Enlasi ฤin + MessageInput Hang up - + Fini @@ -900,7 +1049,7 @@ Ekzemplo: https://servilo.mia:8787 Send a file - Sendu dosieron + Sendi dosieron @@ -908,9 +1057,9 @@ Ekzemplo: https://servilo.mia:8787 Skribu mesaฤonโ€ฆ - + Stickers - + Glumarkoj @@ -931,7 +1080,7 @@ Ekzemplo: https://servilo.mia:8787 MessageView - + Edit Redakti @@ -951,74 +1100,81 @@ Ekzemplo: https://servilo.mia:8787 Elektebloj - + + &Copy - + &Kopii - + + Copy &link location - + Kopii celon de &ligilo - + Re&act - + Re&agi Repl&y - + Re&spondi &Edit - + R&edakti Read receip&ts - + K&vitancoj &Forward - + &Plusendi &Mark as read - + &Marki legita View raw message - Vidi krudan mesaฤon + Vidi krudan mesaฤon View decrypted raw message - Vidi malฤ‰ifritan krudan mesaฤon + Vidi malฤ‰ifritan krudan mesaฤon Remo&ve message - + &Forigi mesaฤon &Save as - + Kon&servi kiel &Open in external program - + &Malfermi per aparta programo Copy link to eve&nt - + Kopii ligilon al oka&zo + + + + &Go to quoted message + &Iri al citita mesaฤo @@ -1034,7 +1190,12 @@ Ekzemplo: https://servilo.mia:8787 Ricevita kontrolpeto - + + 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.) + + + + 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? Por vidigi al aliuloj, kiuj viaj aparatoj vere apartenas al vi, vi povas ilin kontroli. Tio ankaลญ funkciigus memagan savkopiadon de ลlosiloj. ฤˆu vi volas kontroli aparaton %1 nun? @@ -1079,33 +1240,29 @@ Ekzemplo: https://servilo.mia:8787 Akcepti + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 sendis ฤ‰ifritan mesaฤon - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - * %1 %2 - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message %1 respondis: %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,10 +1290,10 @@ Ekzemplo: https://servilo.mia:8787 No microphone found. - Neniu mikrofono troviฤis. + Neniu mikrofono troviฤis. - + Voice Voฤ‰e @@ -1167,9 +1324,9 @@ Ekzemplo: https://servilo.mia:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. - + Krei unikan profilon, kiu permesos al vi saluti kelkajn kontojn samtempe, kaj startigi plurajn nhekojn. @@ -1182,21 +1339,37 @@ Ekzemplo: https://servilo.mia:8787 nomo de profilo + + ReadReceipts + + + Read receipts + Kvitancoj + + + + ReadReceiptsModel + + + Yesterday, %1 + Hieraลญ, %1 + + RegisterPage - + Username Uzantonomo - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. La uzantonomo devas ne esti malplena, kaj devas enhavi nur la signojn aโ€“z, 0โ€“9, ., _, =, -, kaj /. - + Password Pasvorto @@ -1216,7 +1389,7 @@ Ekzemplo: https://servilo.mia:8787 Hejmservilo - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. Servilo, kiu permesas registriฤon. ฤˆar Matrikso estas federa, vi bezonas unue trovi servilon, kie vi povus registriฤi, aลญ gastigi vian propran. @@ -1226,52 +1399,42 @@ Ekzemplo: https://servilo.mia:8787 REGISTRIฤœI - - No supported registration flows! - Neniuj subtenataj manieroj de registriฤo! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - Unu aลญ pliaj kampoj havas nevalidajn enigojn. Bonvolu korekti la problemojn kaj reprovi. - - - + Autodiscovery failed. Received malformed response. - + Malsukcesis memaga trovado. Ricevis misformitan respondon. - + Autodiscovery failed. Unknown error when requesting .well-known. - + Malsukcesis memaga trovado. Okazis nekonata eraro dum petado. .well-known. - + The required endpoints were not found. Possibly not a Matrix server. - + La bezonataj konektaj lokoj ne troviฤis. Eble tio ne estas Matriksa servilo. Received malformed response. Make sure the homeserver domain is valid. - + Ricevis misformitan respondon. Certiฤu, ke retnomo de la hejmservilo estas valida. An unknown error occured. Make sure the homeserver domain is valid. - + Okazis nekonata eraro. Certiฤu, ke retnomo de la hejmservilo estas valida. - + Password is not long enough (min 8 chars) Pasvorto nesufiฤ‰e longas (almenaลญ 8 signoj) - + Passwords don't match Pasvortoj ne akordas - + Invalid server name Nevalida nomo de servilo @@ -1279,7 +1442,7 @@ Ekzemplo: https://servilo.mia:8787 ReplyPopup - + Close Fermi @@ -1289,148 +1452,199 @@ Ekzemplo: https://servilo.mia:8787 Nuligi redakton + + RoomDirectory + + + Explore Public Rooms + Esplori publikajn ฤ‰ambrojn + + + + Search for public rooms + Serฤ‰i publikajn ฤ‰ambrojn + + + + Choose custom homeserver + + + RoomInfo - + no version stored - + neniu versio konservita RoomList - + New tag - + Nova etikedo Enter the tag you want to use: - - - - - Leave Room - - - - - Are you sure you want to leave this room? - + Enigu la etikedon, kiun vi volas uzi: Leave room - Eliri el ฤ‰ambro + Eliri el ฤ‰ambro Tag room as: - Etikedi ฤ‰ambron: + Etikedi ฤ‰ambron: Favourite - Preferata + Elstara Low priority - + Malalta prioritato Server notice - + Avizo de servilo Create new tag... - + Krei novan etikedonโ€ฆ - + Status Message - + Statmesaฤo Enter your status message: - + Enigu vian statmesaฤon: Profile settings - + Agordoj de profilo Set status message + Meti statmesaฤon + + + + Logout + Adiaลญi + + + + Encryption not set up + Cross-signing setup has not run yet. - - Logout - Adiaลญi + + Unverified login + The user just signed in with this device and hasn't verified their master key. + - + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Fermi + + + Start a new chat - Komenci novan babilon + Komenci novan babilon Join a room - Aliฤi ฤ‰ambron + Aliฤi al ฤ‰ambro Create a new room - + Krei novan ฤ‰ambron Room directory - ฤˆambra dosierujo + Katalogo de ฤ‰ambroj - + User settings - Agordoj de uzanto + Agordoj de uzanto RoomMembers - + Members of %1 - + Anoj de %1 - + %n people in %1 Summary above list of members - - - + + %n persono en %1 + %n personoj en %1 Invite more people - + Inviti pliajn personojn + + + + This room is not encrypted! + ฤˆi tiu ฤ‰ambro ne estas ฤ‰ifrata! + + + + This user is verified. + ฤˆi tiu uzanto estas kontrolita. + + + + This user isn't verified, but is still using the same master key from the first time you met. + ฤˆi tiu uzanto ne estas kontrolita, sed ankoraลญ uzas la saman ฤ‰efan ลlosilon ekde kiam vi renkontiฤis. + + + + This user has unverified devices! + ฤˆi tiu uzanto havas nekontrolitajn aparatojn! RoomSettings - + Room Settings Agordoj de ฤ‰ambro - + %1 member(s) %1 ฤ‰ambrano(j) @@ -1460,7 +1674,12 @@ Ekzemplo: https://servilo.mia:8787 ฤˆiuj mesaฤoj - + + Room access + Aliro al ฤ‰ambro + + + Anyone and guests ฤˆiu ajn, inkluzive gastojn @@ -1475,7 +1694,17 @@ Ekzemplo: https://servilo.mia:8787 Invititoj - + + By knocking + Per frapado + + + + Restricted by membership in other rooms + Limigita de aneco en aliaj ฤ‰ambroj + + + Encryption ฤˆifrado @@ -1493,17 +1722,17 @@ Ekzemplo: https://servilo.mia:8787 Sticker & Emote Settings - + Agordoj de glumarkoj kaj mienetoj Change - + ลœanฤi Change what packs are enabled, remove packs or create new ones - + ลœalti, forigi, aลญ krei novajn pakojn @@ -1521,19 +1750,19 @@ Ekzemplo: https://servilo.mia:8787 Versio de ฤ‰ambro - + Failed to enable encryption: %1 Malsukcesis ลalti ฤ‰ifradon: %1 - + Select an avatar Elektu bildon de ฤ‰ambro All Files (*) - ฤˆiuj dosieroj (*) + ฤˆiuj dosieroj (*) @@ -1546,8 +1775,8 @@ Ekzemplo: https://servilo.mia:8787 Eraris legado de dosiero: %1 - - + + Failed to upload image: %s Malsukcesis alลuti bildon: %s @@ -1555,18 +1784,46 @@ Ekzemplo: https://servilo.mia:8787 RoomlistModel - + Pending invite. - + Atendanta invito. - + Previewing this room + Antaลญrigardante ฤ‰i tiun ฤ‰ambron + + + + No preview available + Neniu antaลญrigardo disponeblas + + + + Root + + + Please enter your login password to continue: - - No preview available + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. @@ -1590,18 +1847,18 @@ Ekzemplo: https://servilo.mia:8787 Include your camera picture-in-picture - + Enigi vian filmilon en la filmon Request remote camera - + Peti foran filmilon View your callee's camera like a regular video call - + Vidi la filmilon de via vokato kiel en ordinara vidvoko @@ -1611,12 +1868,12 @@ Ekzemplo: https://servilo.mia:8787 Share - + Vidigi Preview - + Antaลญrigardi @@ -1624,6 +1881,121 @@ Ekzemplo: https://servilo.mia:8787 Nuligi + + SecretStorage + + + Failed to connect to secret storage + Malsukcesis konektiฤi al sekreta deponejo + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Malsukcesis ฤisdatigi bildopakon: %1 + + + + Failed to delete old image pack: %1 + Malsukcesis forigi malnovan bildopakon: %1 + + + + Failed to open image: %1 + Malsukcesis malfermi bildon: %1 + + + + Failed to upload image: %1 + Malsukcesis alลuti bildon: %1 + + StatusIndicator @@ -1653,7 +2025,7 @@ Ekzemplo: https://servilo.mia:8787 Search - Serฤ‰u + Serฤ‰u @@ -1661,34 +2033,34 @@ Ekzemplo: https://servilo.mia:8787 Successful Verification - + Sukcesis kontrolo Verification successful! Both sides verified their devices! - + Sukcesis kontrolo! Ambaลญ flankoj kontrolis siajn aparatojn! Close - Fermi + Fermi TimelineModel - + Message redaction failed: %1 - + Malsukcesis redaktado de mesaฤo: %1 - + Failed to encrypt event, sending aborted! - + Malsukcesis ฤ‰ifri okazon; sendado nuliฤis! - + Save image Konservi bildon @@ -1708,7 +2080,7 @@ Ekzemplo: https://servilo.mia:8787 Konservi dosieron - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1717,7 +2089,7 @@ Ekzemplo: https://servilo.mia:8787 - + %1 opened the room to the public. %1 malfermis la ฤ‰ambron al publiko. @@ -1727,7 +2099,17 @@ Ekzemplo: https://servilo.mia:8787 %1 ekpostulis inviton por aliฤoj al la ฤ‰amrbo. - + + %1 allowed to join this room by knocking. + %1 permesis aliฤi al ฤ‰i tiu ฤ‰ambro per frapado. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 permesis al anoj de la jenaj ฤ‰ambroj memage aliฤi al ฤ‰i tiu ฤ‰ambro: %2 + + + %1 made the room open to guests. %1 malfermis la ฤ‰ambron al gastoj. @@ -1747,28 +2129,28 @@ Ekzemplo: https://servilo.mia:8787 %1 videbligis historion de la ฤ‰ambro al ฤ‰iuj ฤ‰ambranoj ekde nun. - + %1 set the room history visible to members since they were invited. %1 videbligis historion de la ฤ‰ambro al ฤ‰ambranoj ekde ties invito. - + %1 set the room history visible to members since they joined the room. %1 videbligis historion de la ฤ‰ambro al ฤ‰ambranoj ekde ties aliฤo. %1 has changed the room's permissions. - + %1 ลanฤis permesojn de la ฤ‰ambro. - + %1 was invited. %1 estis invitata. %1 estis invitita. - + %1 changed their avatar. %1 ลanฤis sian avataron. %1 ลanฤis sian profilbildon. @@ -1776,22 +2158,27 @@ Ekzemplo: https://servilo.mia:8787 %1 changed some profile info. - + %1 ลanฤis iujn informojn en profilo. - + %1 joined. %1 aliฤis. - + + %1 joined via authorisation from %2's server. + %1 aliฤis per rajtigo de servilo de %2. + + + %1 rejected their invite. - + %1 rifuzis sian inviton. Revoked the invite to %1. - + Nuligis la inviton por %1. @@ -1801,47 +2188,47 @@ Ekzemplo: https://servilo.mia:8787 Kicked %1. - + Forpelis uzanton %1. Unbanned %1. - + Malforbaris uzanton %1. %1 was banned. - + %1 estas forbarita. - + Reason: %1 - + Kialo: %1 - + %1 redacted their knock. - + %1 forigis sian frapon. - + You joined this room. Vi aliฤis ฤ‰i tiun ฤ‰ambron. - + %1 has changed their avatar and changed their display name to %2. - + %1 ลanฤis sian profilbildon kaj sian prezentan nomon al %2. - + %1 has changed their display name to %2. - + %1 ลanฤis sian prezentan nomon al %2. - + Rejected the knock from %1. - + Rifuzis la frapon de %1. @@ -1852,13 +2239,13 @@ Ekzemplo: https://servilo.mia:8787 %1 knocked. - + %1 frapis. TimelineRow - + Edited Redaktita @@ -1866,70 +2253,87 @@ Ekzemplo: https://servilo.mia:8787 TimelineView - + No room open - + Neniu ฤ‰ambro estas malfermita - + + No preview available + Neniu antaลญrigardo disponeblas + + + %1 member(s) - %1 ฤ‰ambrano(j) + %1 ฤ‰ambrano(j) join the conversation - + aliฤi al interparolo accept invite - + akcepti inviton decline invite - + rifuzi inviton Back to room list - - - - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - + Reen al listo de ฤ‰ambroj TopBar - + Back to room list - + Reen al listo de ฤ‰ambroj - + No room selected + Neniu ฤ‰ambro estas elektita + + + + This room is not encrypted! + ฤˆi tiu ฤ‰ambro ne estas ฤ‰ifrata! + + + + This room contains only verified devices. + ฤˆi tiu ฤ‰ambro enhavas nur kontrolitajn aparatojn. + + + + This room contains verified devices and devices which have never changed their master key. - + + This room contains unverified devices! + ฤˆi tiu ฤ‰ambro enhavas nekontrolitajn aparatojn! + + + Room options - + Elektebloj de ฤ‰ambro Invite users - + Inviti uzantojn Members - Membroj + Anoj @@ -1952,56 +2356,146 @@ Ekzemplo: https://servilo.mia:8787 Quit + ฤˆesigi + + + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Bonvolu enigi validan registran pecon. + + + + Invalid token UserProfile - + Global User Profile - + ฤˆiea profilo de uzanto Room User Profile - + ฤˆambra profilo de uzanto - - + + Change avatar globally. + ลœanฤi bildon ฤ‰ie. + + + + Change avatar. Will only apply to this room. + ลœanฤi bildon. Efektiviฤos nur en ฤ‰i tiu ฤ‰ambro. + + + + Change display name globally. + ลœanฤi prezentan nomon ฤ‰ie. + + + + Change display name. Will only apply to this room. + ลœanฤi prezentan nomon. Efektiviฤos nur en ฤ‰i tiu ฤ‰ambro. + + + + Room: %1 + ฤˆambro: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + ฤˆi tio estas profilo speciala por ฤ‰ambro. La nomo kaj profilbildo de la uzanto povas esti malsamaj de siaj ฤ‰ieaj versioj. + + + + Open the global profile for this user. + Malfermi la ฤ‰iean profilon de ฤ‰i tiu uzanto. + + + + Verify - + Kontroli - - Ban the user - - - - - Start a private chat - + + Start a private chat. + Komenci privatan babilon. - Kick the user + Kick the user. + Forpeli la uzanton. + + + + Ban the user. + Forbari la uzanton. + + + + Refresh device list. - + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify + Malkontroli + + + + Sign out device %1 - + + You signed out this device. + + + + Select an avatar Elektu profilbildon All Files (*) - ฤˆiuj dosieroj (*) + ฤˆiuj dosieroj (*) @@ -2017,43 +2511,43 @@ Ekzemplo: https://servilo.mia:8787 UserSettings - - + + Default - + Implicita UserSettingsPage - + Minimize to tray - + Etigi al plato Start in tray - + Komenci ete sur pleto - + Group's sidebar - + Flanka breto de grupoj - + Circular Avatars - + Rondaj profilbildoj - + profile: %1 - + profilo: %1 - + Default - + Implicita @@ -2063,7 +2557,7 @@ Ekzemplo: https://servilo.mia:8787 Cross Signing Keys - + ลœlosiloj por delegaj subskriboj @@ -2076,7 +2570,7 @@ Ekzemplo: https://servilo.mia:8787 ELลœUTI - + Keep the application running in the background after closing the client window. Daลญrigi la aplikaฤตon fone post fermo de la klienta fenestro. @@ -2092,6 +2586,16 @@ OFF - square, ON - Circle. ลœanฤas la aspekton de profilbildoj de uzantoj en babilujo. NE โ€“ kvadrataj, JES โ€“ rondaj. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2122,7 +2626,7 @@ be blurred. malklariฤos. - + Privacy screen timeout (in seconds [0 - 3600]) Atendo ฤis privateca ลirmilo (0โ€“3600 sekundoj) @@ -2134,7 +2638,7 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds Tempo de atendo (en sekundoj) ekde senfokusiฤo de la fenestro, post kiu la enhavo malklariฤos. Agordu al 0 por malklarigi enhavon tuj post senfokusiฤo. -Maksimuma valoro estas 1 horo (3600 sekundoj). +Maksimuma valoro estas 1 horo (3600 sekundoj) @@ -2189,7 +2693,7 @@ kiujn vi silentigis, ankoraลญ estos ordigitaj laลญ tempo, ฤ‰ar vi probable ne pensas ilin same gravaj kiel la aliaj ฤ‰ambroj. - + Read receipts Kvitancoj @@ -2201,7 +2705,7 @@ Status is displayed next to timestamps. Stato estas montrita apud tempindikoj. - + Send messages as Markdown Sendi mesaฤojn Markdaลญne @@ -2214,6 +2718,11 @@ Kun ฤ‰i tio malลaltita, ฤ‰iuj mesaฤoj sendiฤas en plata teksto. + Play animated images only on hover + Ludi movbildojn nur sub musmontrilo + + + Desktop notifications Labortablaj sciigoj @@ -2225,218 +2734,254 @@ Kun ฤ‰i tio malลaltita, ฤ‰iuj mesaฤoj sendiฤas en plata teksto. Alert on notification - + Atentigi pri sciigoj Show an alert when a message is received. This usually causes the application icon in the task bar to animate in some fashion. - + Atentigas je ricevo de mesaฤo. +ฤˆi tio kutime movbildigas la simbolbildon sur la pleto iumaniere. Highlight message on hover - + Emfazi mesaฤojn sub musmontrilo Change the background color of messages when you hover over them. - + ลœanฤi fonkoloron de mesaฤoj sub musmontrilo. Large Emoji in timeline - + Grandaj bildosignoj en historio Make font size larger if messages with only a few emojis are displayed. - + Grandigi tiparon se montriฤas mesaฤoj kun nur kelkaj bildosignoj. - + + Send encrypted messages to verified users only + Sendi ฤ‰ifritajn mesaฤojn nur al kontrolitaj uzantoj + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Postulas, ke uzanto estu kontrolita, por ke ฤi povu ricevi mesaฤojn. ฤˆi tio plibonigas sekurecon, sed iom maloportunigas tutvojan ฤ‰ifradon. + + + Share keys with verified users and devices - + Havigi ลlosilojn al kontrolitaj uzantoj kaj aparatoj - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Memage respondas al petoj de aliaj uzantoj je ลlosiloj, se tiuj uzantoj estas kontrolitaj, eฤ‰ se la aparato ne povus aliri tiujn ลlosilojn alie. + + + + Online Key Backup + Enreta savkopiado de ลlosiloj + + + + Download message encryption keys from and upload to the encrypted online key backup. + Elลutu ฤ‰ifrajn ลlosilojn por mesaฤoj de la ฤ‰ifrita enreta deponejo de ลlosiloj, aลญ alลutu ilin tien. + + + + Enable online key backup + ลœalti enretan savkopiadon de ลlosiloj + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + Aลญtoroj de Nheko rekomendas ne ลalti enretan savkopiadon de ลlosiloj, almenaลญ ฤis simetria enreta savkopiado estos disponebla. ฤˆu vi tamen volas ฤin ลalti? + + + CACHED - + KAลœMEMORITA NOT CACHED - + NE KAลœMEMORITA - + Scale factor - + Skala obligo Change the scale factor of the whole user interface. - + ลœanฤas skalan obligon de la tuta fasado. Font size - + Tipara grando Font Family - + Tipara familio Theme - + Haลญto Ringtone - + Sonoro Set the notification sound to play when a call invite arrives - + Agordi sciigan sonon, kiu aลญdiฤos je invito al voko Microphone - + Mikrofono Camera - + Filmilo Camera resolution - + Distingumo de filmilo Camera frame rate - + Filmerrapido de filmilo Allow fallback call assist server - + Permesi repaลan asistan servilon por vokoj Will use turn.matrix.org as assist when your home server does not offer one. - + Uzos la servilon turn.matrix.org kiel asistanton, kiam via hejma servilo ne disponigos propran. Device ID - + Identigilo de aparato Device Fingerprint - + Fingrospuro de aparato - + Session Keys - + ลœlosiloj de salutaฤตo IMPORT - + ENPORTI EXPORT - + ELPORTI ENCRYPTION - + ฤˆIFRADO - + GENERAL - + ฤœENERALAJ - + INTERFACE - + FASADO - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Ludas vidaลญdaฤตojn kiel GIF-ojn aลญ WEBP-ojn nur sub musmontrilo. + + + Touchscreen mode - + Tuลekrana reฤimo Will prevent text selection in the timeline to make touch scrolling easier. - + Malhelpos elkton de teksto en historio por faciligi rulumadon per tuลoj. Emoji Font Family - + Bildosigna tiparo - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key - + ฤˆefa subskriba ลlosilo Your most important key. You don't need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys. - + Via plej grava ลlosilo. Vi ne bezonas kaลmemori ฤin, ฤ‰ar de kaลmemoro ฤi povus esti pli facile ลtelebla, kaj vi bezonas ฤin nur por ลanฤado de aliaj viaj subskribaj ลlosiloj. User signing key - + Uzanto-subskriba ลlosilo The key to verify other users. If it is cached, verifying a user will verify all their devices. - + ลœlosilo por kontrolado de aliaj uzantoj. Se ฤi estas kaลmemorata, kontrolo de uzanto kontrolos ankaลญ ฤ‰iujn ฤiajn aparatojn. - + Self signing key - + Mem-subskriba ลlosilo The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you. - + La ลlosilo por kontrolado de viaj propraj aparatoj. Se ฤi estas kaลmemorata, kontrolo de unu el viaj aparatoj markos ฤin kontrolita por aliaj viaj aparatoj, kaj por uzantoj, kiuj vin kontrolis. Backup key - + Savkopia ลlosilo The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server. - + La ลlosilo por malฤ‰ifrado de enretaj savkopioj de ลlosiloj. Se ฤi estas kaลmemorata, vi povas ลalti enretan savkopiadon de ลlosiloj por deponi ลlosilojn sekure ฤ‰ifritajn al la servilo. Select a file - Elektu dosieron + Elektu dosieron @@ -2444,46 +2989,54 @@ This usually causes the application icon in the task bar to animate in some fash ฤˆiuj dosieroj (*) - + Open Sessions File - + Malfermi dosieron kun salutaฤตoj - + Error - + Eraro - - + + File Password - + Pasvorto de dosiero - + Enter the passphrase to decrypt the file: - + Enigu pasfrazon por malฤ‰ifri la dosieron: - + The password cannot be empty - + La pasvorto ne povas esti malplena Enter passphrase to encrypt your session keys: - + Enigu pasfrazon por ฤ‰ifri ลlosilojn de via salutaฤตo: File to save the exported session keys - + Dosiero, kien konserviฤos la elportitaj ลloslioj de salutaฤตo + + + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Neniu ฤ‰ifrita privata babilo kun ฤ‰i tiu uzanto troviฤis. Kreu ฤ‰ifritan privatan babilon kun ฤ‰i tiu uzanto kaj reprovu. @@ -2491,27 +3044,27 @@ This usually causes the application icon in the task bar to animate in some fash Waiting for other partyโ€ฆ - + Atendante la aliulonโ€ฆ Waiting for other side to accept the verification request. - + Atendante, ฤis la aliulo akceptos la kontrolpeton. Waiting for other side to continue the verification process. - + Atendante, ฤis la aliulo finos la kontrolon. Waiting for other side to complete the verification process. - + Atendante, ฤis la aliulo finos la kontrolon. Cancel - Nuligi + Nuligi @@ -2556,7 +3109,7 @@ This usually causes the application icon in the task bar to animate in some fash Cancel - Nuligi + Nuligi @@ -2566,27 +3119,27 @@ This usually causes the application icon in the task bar to animate in some fash Topic - Temo + Temo Alias - + Kromnomo Room Visibility - + Videbleco de ฤ‰ambro Room Preset - + Antaลญagordo de ฤ‰ambro Direct Chat - + Individua ฤ‰ambro @@ -2594,53 +3147,22 @@ This usually causes the application icon in the task bar to animate in some fash Open Fallback in Browser - + Iri al foliumilo por la alternativa metodo Cancel - Nuligi + Nuligi Confirm - + Konfirmi Open the fallback, follow the steps and confirm after completing them. - - - - - dialogs::JoinRoom - - - Join - Aliฤi - - - - Cancel - Nuligi - - - - Room ID or alias - - - - - dialogs::LeaveRoom - - - Cancel - Nuligi - - - - Are you sure you want to leave? - + Iru al la alternativa metodo, sekvu la paลojn, kaj fininte ilin, konfirmu. @@ -2648,7 +3170,7 @@ This usually causes the application icon in the task bar to animate in some fash Cancel - Nuligi + Nuligi @@ -2661,19 +3183,21 @@ This usually causes the application icon in the task bar to animate in some fash Upload - + Alลuti Cancel - Nuligi + Nuligi Media type: %1 Media size: %2 - + Speco de vidaลญdaฤตo: %1 +Grandeco de vidaลญdaฤตo: %2 + @@ -2681,43 +3205,17 @@ Media size: %2 Cancel - Nuligi + Nuligi Confirm - + Konfirmi Solve the reCAPTCHA and press the confirm button - - - - - dialogs::ReadReceipts - - - Read receipts - Kvitancoj - - - - Close - Fermi - - - - dialogs::ReceiptItem - - - Today %1 - Hodiaลญ %1 - - - - Yesterday %1 - Hieraลญ %1 + Solvu la kontrolon de homeco de ยซreCAPTCHAยป kaj premu la konfirman butonon @@ -2725,62 +3223,62 @@ Media size: %2 You sent an audio clip - + Vi sendis sonmesaฤon %1 sent an audio clip - + %1 sendis sonmesaฤon - + You sent an image Vi sendis bildon - + %1 sent an image %1 sendis bildon - + You sent a file Vi sendis dosieron - + %1 sent a file %1 sendis dosieron - + You sent a video Vi sendis filmon - + %1 sent a video %1 sendis filmon - + You sent a sticker Vi sendis glumarkon - + %1 sent a sticker %1 sendis glumarkon - + You sent a notification - + Vi sendis sciigon %1 sent a notification - + %1 sendis sciigon @@ -2788,57 +3286,57 @@ Media size: %2 Vi: %1 - + %1: %2 %1: %2 You sent an encrypted message - + Vi sendis ฤ‰ifritan mesaฤon %1 sent an encrypted message - %1 sendis ฤ‰ifritan mesaฤon + %1 sendis ฤ‰ifritan mesaฤon You placed a call - + Vi vokis - + %1 placed a call - + %1 vokis - + You answered a call - + Vi respondis vokon - + %1 answered a call - + %1 respondis vokon - + You ended a call - + Vi finis vokon - + %1 ended a call - + %1 finis vokon utils - + Unknown Message Type - + Nekonata tipo de mesaฤo diff --git a/resources/langs/nheko_es.ts b/resources/langs/nheko_es.ts index 922e0500..63916a0a 100644 --- a/resources/langs/nheko_es.ts +++ b/resources/langs/nheko_es.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Llamando... @@ -56,7 +56,7 @@ CallInvite - + Video Call Videollamada @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Videollamada @@ -117,7 +117,7 @@ CallManager - + Entire screen Pantalla completa @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 No se pudo invitar al usuario: %1 - + Invited user: %1 Usuario invitado: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. La migraciรณn de la cachรฉ a la versiรณn actual ha fallado. Esto puede deberse a distintos motivos. Por favor, reporte el incidente y mientras tanto intente usar una versiรณn anterior. Tambiรฉn puede probar a borrar la cachรฉ manualmente. - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. Sala %1 creada. - + Confirm invite Confirmar invitaciรณn - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 @@ -182,7 +182,7 @@ - + Kicked user: %1 Se ha expulsado a %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -247,33 +247,35 @@ - + Failed to restore OLM account. Please login again. + + Failed to restore save data. Please login again. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - - + + Please try to login again: %1 - + Failed to join room: %1 - + You joined the room @@ -283,7 +285,7 @@ - + Room creation failed: %1 @@ -293,7 +295,7 @@ - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + Cancelar + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close @@ -645,7 +744,7 @@ InputBar - + Select a file @@ -655,7 +754,7 @@ - + Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ Cancelar + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + + + + + LeaveRoomDialog + + + Leave room + + + + + Are you sure you want to leave? + + + LoginPage @@ -756,25 +881,25 @@ Example: https://server.my:8787 - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -784,35 +909,53 @@ Example: https://server.my:8787 - + An unknown error occured. Make sure the homeserver domain is valid. - + SSO LOGIN - + Empty password - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -862,18 +1005,23 @@ Example: https://server.my:8787 - + + Allow them in + + + + %1 answered the call. - + removed - + %1 ended the call. @@ -901,7 +1049,7 @@ Example: https://server.my:8787 - + Stickers @@ -924,7 +1072,7 @@ Example: https://server.my:8787 MessageView - + Edit @@ -944,17 +1092,19 @@ Example: https://server.my:8787 - + + &Copy - + + Copy &link location - + Re&act @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1072,33 +1232,29 @@ Example: https://server.my:8787 Aceptar + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 No se encontrรณ micrรณfono. - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 - + Password is not long enough (min 8 chars) - + Passwords don't match - + Invalid server name @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + + + + Start a new chat @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 - + User settings @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1414,16 +1607,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1453,7 +1666,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1468,7 +1686,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1514,12 +1742,12 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 - + Select an avatar @@ -1539,8 +1767,8 @@ Example: https://server.my:8787 - - + + Failed to upload image: %s @@ -1548,21 +1776,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1617,6 +1873,121 @@ Example: https://server.my:8787 Cancelar + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1669,18 +2040,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 - + Failed to encrypt event, sending aborted! - + Save image @@ -1700,7 +2071,7 @@ Example: https://server.my:8787 - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1709,7 +2080,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1719,7 +2090,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1739,12 +2120,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1754,7 +2135,7 @@ Example: https://server.my:8787 - + %1 was invited. @@ -1764,7 +2145,7 @@ Example: https://server.my:8787 - + %1 has changed their display name to %2. @@ -1779,12 +2160,17 @@ Example: https://server.my:8787 - + %1 joined. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. @@ -1814,22 +2200,22 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. - + You joined this room. Te has unido a esta sala. - + Rejected the knock from %1. @@ -1848,7 +2234,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1856,12 +2242,17 @@ Example: https://server.my:8787 TimelineView - + No room open - + + No preview available + + + + %1 member(s) @@ -1886,28 +2277,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options @@ -1945,10 +2348,35 @@ Example: https://server.my:8787 + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1958,33 +2386,98 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar @@ -2007,8 +2500,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2016,7 +2509,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2026,22 +2519,22 @@ Example: https://server.my:8787 - + Group's sidebar - + Circular Avatars - + profile: %1 - + Default @@ -2066,7 +2559,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2081,6 +2574,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2109,7 +2612,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts @@ -2175,7 +2678,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown @@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications @@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File - + @@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash - - + + File Password - + Enter the passphrase to decrypt the file: - + The password cannot be empty @@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - Cancelar - - - - Room ID or alias - - - - - dialogs::LeaveRoom - - - Cancel - Cancelar - - - - Are you sure you want to leave? - - - dialogs::Logout @@ -2666,32 +3186,6 @@ Media size: %2 - - dialogs::ReadReceipts - - - Read receipts - - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2705,47 +3199,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2760,7 +3254,7 @@ Media size: %2 - + %1: %2 @@ -2780,27 +3274,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2808,7 +3302,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/langs/nheko_et.ts b/resources/langs/nheko_et.ts index 57eac1c3..0d72ad6b 100644 --- a/resources/langs/nheko_et.ts +++ b/resources/langs/nheko_et.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Helistan... @@ -56,7 +56,7 @@ CallInvite - + Video Call Videokรตne @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Videokรตne @@ -117,7 +117,7 @@ CallManager - + Entire screen Terve ekraan @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 Kutse saatmine kasutajale ei รตnnestunud: %1 - + Invited user: %1 Kutsutud kasutaja: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. Puhverdatud andmete muutmine sobivaks rakenduse praeguse versiooniga ei รตnnestunud. Sellel vรตib olla erinevaid pรตhjuseid. Palun saada meile veateade ja seni kasuta vanemat rakenduse versiooni. Aga kui sa soovid proovida, siis kustuta puhverdatud andmed kรคsitsi. - + Confirm join Kinnita liitumine @@ -151,23 +151,23 @@ Kas sa kindlasti soovid liituda %1 jututoaga? - + Room %1 created. %1 jututuba on loodud. - + Confirm invite Kinnita kutse - + Do you really want to invite %1 (%2)? Kas sa tรตesti soovid saata kutset kasutajale %1 (%2)? - + Failed to invite %1 to %2: %3 Kasutaja %1 kutsumine %2 jututuppa ei รตnnestunud: %3 @@ -182,7 +182,7 @@ Kas sa tรตesti soovid mรผksata kasutaja %1 (%2) jututoast vรคlja? - + Kicked user: %1 Vรคljamรผksatud kasutaja: %1 @@ -197,7 +197,7 @@ Kas sa tรตesti soovid kasutajale %1 (%2) seada suhtluskeeldu? - + Failed to ban %1 in %2: %3 Kasutajale %1 suhtluskeelu seadmine %2 jututoas ei รตnnestunud: %3 @@ -217,7 +217,7 @@ Kas sa tรตesti soovid kasutajalt %1 (%2) eemaldada suhtluskeelu? - + Failed to unban %1 in %2: %3 Kasutajalt %1 suhtluskeelu eemaldamine %2 jututoas ei รตnnestunud: %3 @@ -227,12 +227,12 @@ Suhtluskeeld eemaldatud: %1 - + Do you really want to start a private chat with %1? Kas sa kindlasti soovid alustada otsevestlust kasutajaga %1? - + Cache migration failed! Puhvri versiooniuuendus ebaรตnnestus! @@ -247,33 +247,35 @@ Sinu andmekandjale salvestatud puhvri versioon on uuem, kui kรคesolev Nheko versioon kasutada oskab. Palun tee Nheko uuendus vรตi kustuta puhverdatud andmed. - + Failed to restore OLM account. Please login again. OLM konto taastamine ei รตnnestunud. Palun logi uuesti sisse. + + Failed to restore save data. Please login again. Salvestatud andmete taastamine ei รตnnestunud. Palun logi uuesti sisse. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Krรผptovรตtmete kasutusele vรตtmine ei รตnnestunud. Koduserveri vastus pรคringule: %1 %2. Palun proovi hiljem uuesti. - - + + Please try to login again: %1 Palun proovi uuesti sisse logida: %1 - + Failed to join room: %1 Jututoaga liitumine ei รตnnestunud: %1 - + You joined the room Sa liitusid selle jututoaga @@ -283,7 +285,7 @@ Kutse tagasivรตtmine ei รตnnestunud: %1 - + Room creation failed: %1 Jututoa loomine ei รตnnestunud: %1 @@ -293,7 +295,7 @@ Jututoast lahkumine ei รตnnestunud: %1 - + Failed to kick %1 from %2: %3 Kasutaja %1 vรคljamรผksamine %2 jututoast ei รตnnestunud: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets Dekrรผpti andmed @@ -362,12 +364,12 @@ Andmete dekrรผptimiseks sisesta oma taastevรตti vรตi salafraas: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: Andmete dekrรผptimiseks sisesta oma taastevรตti vรตi salafraas nimega %1: - + Decryption failed Dekrรผptimine ei รตnnestunud @@ -431,7 +433,7 @@ Otsi - + People Inimesed @@ -494,6 +496,49 @@ Mรตlemal pool on รผhesugused emojid! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Selle sรตnumi dekrรผptimiseks pole veel vajalikke vรตtmeid. Me oleme neid serverist automaatselt laadimas, kuid kui sul on vรคga kiire, siis vรตid seda uuesti teha. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Meil on krรผptovรตtmed vaid uuemate sรตnumite jaoks ja seda sรตnumit ei saa dekrรผptida. Sa vรตid proovida vajalikke vรตtmeid eraldi laadida. + + + + There was an internal error reading the decryption key from the database. + Krรผptovรตtmete andmekogust lugemisel tekkis rakenduses viga. + + + + There was an error decrypting this message. + Sรตnumi dekrรผptimisel tekkis viga. + + + + The message couldn't be parsed. + Sรตnumi tรถรถtlemisel tekkis viga. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + Krรผptovรตtit on kasutatud korduvalt! Keegi vรตib proovida siia vestlusesse valesรตnumite lisamist! + + + + Unknown decryption error + Teadmata viga dekrรผptimisel + + + + Request key + Laadi krรผptovรตti + + EncryptionIndicator @@ -513,53 +558,8 @@ - Encrypted by an unverified device - Krรผptitud verifitseerimata seadmes - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Krรผptitud sรผndmus (Dekrรผptimisvรตtmeid ei leidunud) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - -- Krรผptitud sรผndmus (vรตti pole selle indeksi jaoks sobilik) -- - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- Dekrรผptimise viga (megolm'i vรตtmete laadimine andmebaasist ei รตnnestunud) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Dekrรผptimise viga (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Krรผptitud sรผndmus (Tundmatu sรผndmuse tรผรผp) -- - - - - -- Replay attack! This message index was reused! -- - -- Kordusel pรตhinev rรผnne! Selle sรตnumi indeksit on uuesti kasutatud! -- - - - - -- Message by unverified device! -- - -- Sรตnum verifitseerimata seadmest! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Krรผptitud verifitseerimata seadme poolt vรตi krรผptovรตtmed on pรคrit allikast, mida sa pole รผheselt usaldanud (nรคiteks varundatud vรตtmed). @@ -581,17 +581,26 @@ - Device verification timed out. Seadme verifitseerimine aegus. - + Other party canceled the verification. Teine osapool katkestas verifitseerimise. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close Sulge @@ -604,48 +613,138 @@ Suuna sรตnum edasi + + ImagePackEditorDialog + + + Editing image pack + Muudan pildipakki + + + + Add images + Lisa pilte + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Kleepsud (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + Olekuvรตti + + + + Packname + Pildikogu nimi + + + + Attribution + Viide allikale + + + + + Use as Emoji + Kasuta emojina + + + + + Use as Sticker + Kasuta kleepsuna + + + + Shortcode + Lรผhend + + + + Body + Sisu + + + + Remove from pack + Eemalda pakist + + + + Remove + Eemalda + + + + Cancel + Loobu + + + + Save + Salvesta + + ImagePackSettingsDialog Image pack settings - + Pildikogu seadistused - + + Create account pack + Losa kasutajakontokohane pildipakk + + + + New room pack + Uus jututoa pildipakk + + + Private pack - + Isiklik pildipakk Pack from this room - + Pildipakk sellest jututoast Globally enabled pack - + รœldkasutatav pildipakk - + Enable globally - + Luba kasutada รผldiselt Enables this pack to be used in all rooms - + Sellega vรตimaldad pildipaki kasutamist kรตikides jututubades - + + Edit + Muuda + + + Close - Sulge + Sulge InputBar - + Select a file Vali fail @@ -655,7 +754,7 @@ Kรตik failid (*) - + Failed to upload media. Please try again. Meediafailide รผleslaadimine ei รตnnestunud. Palun proovi uuesti. @@ -663,36 +762,62 @@ InviteDialog - + Invite users to %1 - + Kutsu kasutajaid %1 jututuppa User ID to invite - Kasutajatunnus, kellele soovid kutset saata + Kasutajatunnus, kellele soovid kutset saata @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @kadri:matrix.org Add - + Lisa Invite - + Saada kutse Cancel + Loobu + + + + JoinRoomDialog + + + Join room + + + Room ID or alias + Jututoa tunnus vรตi alias + + + + LeaveRoomDialog + + + Leave room + Lahku jututoast + + + + Are you sure you want to leave? + Kas sa oled kindel, et soovid lahkuda? + LoginPage @@ -760,25 +885,25 @@ Nรคiteks: https://server.minu:8787 LOGI SISSE - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org Sisestatud Matrix'i kasutajatunnus on vigane - peaks olema @kasutaja:server.tld - + Autodiscovery failed. Received malformed response. Koduserveri automaatne tuvastamine ei รตnnestunud: pรคringuvastus oli vigane. - + Autodiscovery failed. Unknown error when requesting .well-known. Koduserveri automaatne tuvastamine ei รตnnestunud: tundmatu viga .well-known pรคringu tegemisel. - + The required endpoints were not found. Possibly not a Matrix server. Protokolli jรคrgi nรตutavaid lรตpppunkte ei leidunud. Ilmselt pole tegemist Matrix'i serveriga. @@ -788,35 +913,53 @@ Nรคiteks: https://server.minu:8787 Pรคringule sain tagasi vigase vastuse. Palun kontrolli, et koduserveri domeen oleks รตige. - + An unknown error occured. Make sure the homeserver domain is valid. Tekkis teadmata viga. Palun kontrolli, et koduserveri domeen on รตige. - + SSO LOGIN รœHEKORDNE SISSELOGIMINE - + Empty password Tรผhi salasรตna - + SSO login failed รœhekordne sisselogimine ei รตnnestunud + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + Encryption enabled Krรผptimine on kasutusel - + room name changed to: %1 jututoa uus nimi on: %1 @@ -866,18 +1009,23 @@ Nรคiteks: https://server.minu:8787 รœhendan kรตnetโ€ฆ - + + Allow them in + Luba neid + + + %1 answered the call. %1 vastas kรตnele. - + removed eemaldatud - + %1 ended the call. %1 lรตpetas kรตne. @@ -905,9 +1053,9 @@ Nรคiteks: https://server.minu:8787 Kirjuta sรตnumโ€ฆ - + Stickers - + Kleepsud @@ -928,7 +1076,7 @@ Nรคiteks: https://server.minu:8787 MessageView - + Edit Muuda @@ -948,17 +1096,19 @@ Nรคiteks: https://server.minu:8787 Valikud - + + &Copy &Kopeeri - + + Copy &link location Kopeeri &lingi asukoht - + Re&act Re&ageeri @@ -1017,6 +1167,11 @@ Nรคiteks: https://server.minu:8787 Copy link to eve&nt Kopeeri sรผndmuse li&nk + + + &Go to quoted message + &Vaata tsiteeritud sรตnumit + NewVerificationRequest @@ -1031,7 +1186,12 @@ Nรคiteks: https://server.minu:8787 Saabus verifitseerimispรคring - + + 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.) + + + + 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? Selleks, et muud kasutajad automaatselt usaldaks sinu seadmeid, peaksid nad verifitseerima. Samaga muutub ka krรผptovรตtmete varundus automaatseks. Kas verifitseerime seadme %1? @@ -1076,33 +1236,29 @@ Nรคiteks: https://server.minu:8787 Nรตustu + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 saatis krรผptitud sรตnumi - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - * %1 %2 - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message %1 vastas: %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,7 +1289,7 @@ Nรคiteks: https://server.minu:8787 Ei suuda tuvastada mikrofoni. - + Voice Hรครคlkรตne @@ -1164,7 +1320,7 @@ Nรคiteks: https://server.minu:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. Loo unikaalne profiil, mis vรตimaldab sul logida samaaegselt sisse erinevatele kasutajakontodele ning kรคivitada mitu Nheko programmiakent. @@ -1179,21 +1335,37 @@ Nรคiteks: https://server.minu:8787 Profiili nimi + + ReadReceipts + + + Read receipts + Lugemisteatised + + + + ReadReceiptsModel + + + Yesterday, %1 + Eile, %1 + + RegisterPage - + Username Kasutajanimi - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. Kasutajanimi ei tohi olla tรผhi ning vรตib sisaldada vaid a-z, 0-9, ., _, =, -, / tรคhemรคrke. - + Password Salasรตna @@ -1213,7 +1385,7 @@ Nรคiteks: https://server.minu:8787 Koduserver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. See on server, kus sa oma kasutajakonto registreerid. Kuna Matrix on hajutatud suhtlusvรตrk, siis esmalt pead leidma sulle sobiliku koduserveri vรตi panema pรผsti tรคitsa oma enda koduserveri. @@ -1223,27 +1395,17 @@ Nรคiteks: https://server.minu:8787 REGISTREERI - - No supported registration flows! - Selline registreerimise tรถรถvoog pole toetatud! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - รœhel vรตi enamal andmevรคljal on vigane vรครคrtus. Palun paranda vead ja proovi uuesti. - - - + Autodiscovery failed. Received malformed response. Koduserveri automaatne tuvastamine ei รตnnestunud: pรคringuvastus oli vigane. - + Autodiscovery failed. Unknown error when requesting .well-known. Koduserveri automaatne tuvastamine ei รตnnestunud: tundmatu viga .well-known pรคringu tegemisel. - + The required endpoints were not found. Possibly not a Matrix server. Protokolli jรคrgi nรตutavaid lรตpppunkte ei leidunud. Ilmselt pole tegemist Matrix'i serveriga. @@ -1258,17 +1420,17 @@ Nรคiteks: https://server.minu:8787 Tekkis teadmata viga. Palun kontrolli, et koduserveri domeen on รตige. - + Password is not long enough (min 8 chars) Salasรตna pole piisavalt pikk (vรคhemalt 8 tรคhemรคrki) - + Passwords don't match Salasรตnad ei klapi omavahel - + Invalid server name Vigane koduserveri nimi @@ -1276,7 +1438,7 @@ Nรคiteks: https://server.minu:8787 ReplyPopup - + Close Sulge @@ -1286,10 +1448,28 @@ Nรคiteks: https://server.minu:8787 Tรผhista muudatused + + RoomDirectory + + + Explore Public Rooms + Tutvu avalike jututubadega + + + + Search for public rooms + Otsi avalikke jututube + + + + Choose custom homeserver + + + RoomInfo - + no version stored salvestatud versiooni ei leidu @@ -1297,7 +1477,7 @@ Nรคiteks: https://server.minu:8787 RoomList - + New tag Uus silt @@ -1306,16 +1486,6 @@ Nรคiteks: https://server.minu:8787 Enter the tag you want to use: Kirjuta silt, mida soovid kasutada: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1347,7 +1517,7 @@ Nรคiteks: https://server.minu:8787 Loo uus silt... - + Status Message Olekuteade @@ -1367,12 +1537,35 @@ Nรคiteks: https://server.minu:8787 Sisesta olekuteade - + Logout Logi vรคlja - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Sulge + + + Start a new chat Alusta uut vestlust @@ -1392,7 +1585,7 @@ Nรคiteks: https://server.minu:8787 Jututubade loend - + User settings Kasutaja seadistused @@ -1400,34 +1593,54 @@ Nรคiteks: https://server.minu:8787 RoomMembers - + Members of %1 - + %1 jututoa liikmed - + %n people in %1 Summary above list of members - - - + + %n osaline %1 jututoas + %n osalist %1 jututoas Invite more people - + Kutsu veel liikmeid + + + + This room is not encrypted! + See jututuba on krรผptimata! + + + + This user is verified. + See kasutaja on verifitseeritud. + + + + This user isn't verified, but is still using the same master key from the first time you met. + See kasutaja ei ole verifitseeritud, kuid ta kasutab jรคtkuvalt krรผpto jaoks juurvรตtmeid sellest ajast, kui te kohtusite. + + + + This user has unverified devices! + Sellel kasutajal on verifitseerimata seadmeid! RoomSettings - + Room Settings Jututoa seadistused - + %1 member(s) %1 liige(t) @@ -1457,7 +1670,12 @@ Nรคiteks: https://server.minu:8787 Kรตik sรตnumid - + + Room access + Ligipรครคs jututuppa + + + Anyone and guests Kรตik (sealhulgas kรผlalised) @@ -1472,7 +1690,17 @@ Nรคiteks: https://server.minu:8787 Kutsutud kasutajad - + + By knocking + Koputades + + + + Restricted by membership in other rooms + Piiratud teiste jututubade liikmelisusega + + + Encryption Krรผptimine @@ -1490,17 +1718,17 @@ Nรคiteks: https://server.minu:8787 Sticker & Emote Settings - + Kleepsude ja emotikonide seadistused Change - + Muuda Change what packs are enabled, remove packs or create new ones - + Muuda missugused efektipakid on kasutusel, eemalda neid ja loo uusi @@ -1518,12 +1746,12 @@ Nรคiteks: https://server.minu:8787 Jututoa versioon - + Failed to enable encryption: %1 Krรผptimise kasutuselevรตtmine ei รตnnestunud: %1 - + Select an avatar Vali tunnuspilt @@ -1543,8 +1771,8 @@ Nรคiteks: https://server.minu:8787 Viga faili lugemisel: %1 - - + + Failed to upload image: %s Viga faili รผleslaadimisel: %1 @@ -1552,18 +1780,46 @@ Nรคiteks: https://server.minu:8787 RoomlistModel - + Pending invite. - + Ootel kutse. - + Previewing this room + Jututoa eelvaade + + + + No preview available + Eelvaade pole saadaval + + + + Root + + + Please enter your login password to continue: - - No preview available + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. @@ -1621,6 +1877,121 @@ Nรคiteks: https://server.minu:8787 Loobu + + SecretStorage + + + Failed to connect to secret storage + รœhenduse loomine vรตtmehoidlaga ei รตnnestunud + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Pildipaki uuendamine ei รตnnestunud: %1 + + + + Failed to delete old image pack: %1 + Vana pildipaki kustutamine ei รตnnestunud: %1 + + + + Failed to open image: %1 + Pildi avamine ei รตnnestunud: %1 + + + + Failed to upload image: %1 + Faili รผleslaadimine ei รตnnestunud: %1 + + StatusIndicator @@ -1649,7 +2020,7 @@ Nรคiteks: https://server.minu:8787 Search - Otsi + Otsi @@ -1673,18 +2044,18 @@ Nรคiteks: https://server.minu:8787 TimelineModel - + Message redaction failed: %1 Sรตnumi รผmbersรตnastamine ebaรตnnestus: %1 - + Failed to encrypt event, sending aborted! Sรผndmuse krรผptimine ei รตnnestunud, katkestame saatmise! - + Save image Salvesta pilt @@ -1704,7 +2075,7 @@ Nรคiteks: https://server.minu:8787 Salvesta fail - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1713,7 +2084,7 @@ Nรคiteks: https://server.minu:8787 - + %1 opened the room to the public. %1 tegi jututoa avalikuks. @@ -1723,7 +2094,17 @@ Nรคiteks: https://server.minu:8787 %1 seadistas, et selle jututoaga liitumine eeldab kutset. - + + %1 allowed to join this room by knocking. + %1 pรครคses jututuppa peale uksele koputamist. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 lubas jรคrgmiste jututubade liikmetel selle jututoaga liituda: %2 + + + %1 made the room open to guests. %1 muutis selle jututoa kรผlalistele ligipรครคsetavaks. @@ -1743,12 +2124,12 @@ Nรคiteks: https://server.minu:8787 %1 muutis, et selle jututoa ajalugu saavad lugeda kรตik liikmed alates praegusest ajahetkest. - + %1 set the room history visible to members since they were invited. %1 muutis, et selle jututoa ajalugu saavad lugeda kรตik liikmed alates oma kutse saatmisest. - + %1 set the room history visible to members since they joined the room. %1 muutis, et selle jututoa ajalugu saavad lugeda kรตik liikmed alates jututoaga liitumise hetkest. @@ -1758,12 +2139,12 @@ Nรคiteks: https://server.minu:8787 %1 muutis selle jututoa รตigusi. - + %1 was invited. %1 sai kutse. - + %1 changed their avatar. %1 muutis oma tunnuspilti. @@ -1773,12 +2154,17 @@ Nรคiteks: https://server.minu:8787 %1 muutis oma profiili. - + %1 joined. %1 liitus jututoaga. - + + %1 joined via authorisation from %2's server. + %1 liitus peale autentimist serverist %2. + + + %1 rejected their invite. %1 lรผkkas liitumiskutse tagasi. @@ -1808,32 +2194,32 @@ Nรคiteks: https://server.minu:8787 Kasutaja %1 sai suhtluskeelu. - + Reason: %1 Pรตhjus: %1 - + %1 redacted their knock. %1 muutis oma koputust jututoa uksele. - + You joined this room. Sa liitusid jututoaga. - + %1 has changed their avatar and changed their display name to %2. %1 muutis oma tunnuspilti ja seadistas uueks kuvatavaks nimeks %2. - + %1 has changed their display name to %2. %1 seadistas uueks kuvatavaks nimeks %2. - + Rejected the knock from %1. Lรผkkas tagasi %1 koputuse jututoa uksele. @@ -1852,7 +2238,7 @@ Nรคiteks: https://server.minu:8787 TimelineRow - + Edited Muudetud @@ -1860,29 +2246,34 @@ Nรคiteks: https://server.minu:8787 TimelineView - + No room open รœhtegi jututuba pole avatud - + + No preview available + Eelvaade pole saadaval + + + %1 member(s) %1 liige(t) join the conversation - + liitu vestlusega accept invite - + vรตta kutse vastu decline invite - + lรผkka kutse tagasi @@ -1890,28 +2281,40 @@ Nรคiteks: https://server.minu:8787 Tagasi jututubade loendisse - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - รœhtegi krรผptitud vestlust selle kasutajaga ei leidunud. Palun loo temaga krรผptitud vestlus ja proovi uuesti. - - TopBar - + Back to room list Tagasi jututubade loendisse - + No room selected Jututuba on valimata - + + This room is not encrypted! + See jututuba on krรผptimata! + + + + This room contains only verified devices. + Selles jututoas on vaid verifitseeritud seadmed. + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + Selles jututoas leidub verifitseerimata seadmeid! + + + Room options Jututoa valikud @@ -1949,10 +2352,35 @@ Nรคiteks: https://server.minu:8787 Lรตpeta tรถรถ + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Registreerimiseks palun sisesta kehtiv tunnusluba. + + + + Invalid token + + + UserProfile - + Global User Profile รœldine kasutajaprofiil @@ -1962,33 +2390,98 @@ Nรคiteks: https://server.minu:8787 Kasutajaprofiil jututoas - - + + Change avatar globally. + Muuda oma tunnuspilti kรตikjal. + + + + Change avatar. Will only apply to this room. + Muuda oma tunnuspilti vaid selles jututoas. + + + + Change display name globally. + Muuda oma kuvatavat nime kรตikjal. + + + + Change display name. Will only apply to this room. + Muuda oma kuvatavat nime vaid selles jututoas. + + + + Room: %1 + Jututuba: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + See kasutajaprofiil on vaid selle jututoa kohane. Kasutaja kuvatav nimi ja tunnuspilt vรตivad muudes jutubades olla teistsugused. + + + + Open the global profile for this user. + Vaata selle kasutaja รผldist profiili. + + + + Verify Verifitseeri - - Ban the user - Sea kasutajale suhtluskeeld - - - - Start a private chat - Alusta privaatset vestlust + + Start a private chat. + Alusta privaatset vestlust. - Kick the user - Mรผksa kasutaja vรคlja + Kick the user. + Mรผksa kasutaja vรคlja. - + + Ban the user. + Sea kasutajale suhtluskeeld. + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify Vรตta verifitseerimine tagasi - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar Vali tunnuspilt @@ -2011,8 +2504,8 @@ Nรคiteks: https://server.minu:8787 UserSettings - - + + Default Vaikimisi @@ -2020,7 +2513,7 @@ Nรคiteks: https://server.minu:8787 UserSettingsPage - + Minimize to tray Vรคhenda tegumiribale @@ -2030,22 +2523,22 @@ Nรคiteks: https://server.minu:8787 Kรคivita tegumiribalt - + Group's sidebar Rรผhmade kรผljepaan - + Circular Avatars รœmmargused tunnuspildid - + profile: %1 Profiil: %1 - + Default Vaikimisi @@ -2070,7 +2563,7 @@ Nรคiteks: https://server.minu:8787 ALLALAADIMISED - + Keep the application running in the background after closing the client window. Peale akna sulgemist jรคta rakendus taustal tรถรถle. @@ -2086,6 +2579,16 @@ OFF - square, ON - Circle. Muuda vestlustes kuvatavate tunnuspiltide kuju. Vรคljalรผlitatuna - ruut, sisselรผlitatuna - รผmmargune. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2116,7 +2619,7 @@ be blurred. siis ajajoone vaade hรคgustub. - + Privacy screen timeout (in seconds [0 - 3600]) Viivitus privaatsussirmi sisselรผlitamisel (sekundites [0 - 3600]) @@ -2176,7 +2679,7 @@ Kui see valik on vรคlja lรผlitatud, siis jututoad jรคrjestatakse viimati saanunu Kui see valik on sisse lรผlitatud, siis teavitustega jututoad (pisike รผmmargune numbrifa ikoon) jรคrjestatakse esimesena. Sinu poolt summutatud jututoad jรคrjestatakse ikkagi ajatempli alusel, sest sa ei pea neid teistega vรตrreldes piisavalt tรคhtsaks. - + Read receipts Lugemisteatised @@ -2188,7 +2691,7 @@ Status is displayed next to timestamps. Lugemise olekut kuvatakse ajatempli kรตrval. - + Send messages as Markdown Saada sรตnumid Markdown-vormindusena @@ -2201,6 +2704,11 @@ Kui Markdown ei ole kasutusel, siis saadetakse kรตik sรตnumid vormindamata tekst + Play animated images only on hover + Esita liikuvaid pilte vaid siis, kui kursor on pildi kohal + + + Desktop notifications Tรถรถlauakeskkonna teavitused @@ -2242,12 +2750,47 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Tee sรตnumi font suuremaks, kui sรตnumis on vaid mรตned emojid. - + + Send encrypted messages to verified users only + Saada krรผptitud sรตnumeid vaid verifitseeritud kasutajatele + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Selle tingimuse alusel peab kasutaja olema krรผptitud sรตnumivahetuse jaoks verifitseeritud. Niisugune nรตue parandab turvalisust, kuid teeb lรคbiva krรผptimise natuke ebamugavamaks. + + + Share keys with verified users and devices Jaga vรตtmeid verifitseeritud kasutajate ja seadmetega - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Kui teised kasutajad on verifitseeritud, siis luba automaatselt vastata nende krรผptovรตtmete pรคringutele isegi siis, kui too seade ei peaks saama neid vรตtmeid kasutada. + + + + Online Key Backup + Krรผptovรตtmete varundus vรตrgus + + + + Download message encryption keys from and upload to the encrypted online key backup. + Luba krรผptitud vรตtmete varunduseks laadida sรตnumite krรผptovรตtmeid sinu serverisse vรตi sinu serverist. + + + + Enable online key backup + Vรตta kasutusele krรผptovรตtmete varundus vรตrgus + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + Seni kuni sรผmmetriline krรผptovรตtmete varundamine pole teostatav, siis Nheko arendajad ei soovita krรผptovรตtmeid vรตrgus salvestada. Kas ikkagi jรคtkame? + + + CACHED PUHVERDATUD @@ -2257,7 +2800,7 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim PUHVERDAMATA - + Scale factor Mastaabitegur @@ -2332,7 +2875,7 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Seadme sรตrmejรคlg - + Session Keys Sessioonivรตtmed @@ -2352,17 +2895,22 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim KRรœPTIMINE - + GENERAL รœLDISED SEADISTUSED - + INTERFACE LIIDES - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Esitame liikuvaid GIF ja WEBP pilte vaid siis, kui kursor on pildi kohal. + + + Touchscreen mode Puuteekraani reลพiim @@ -2377,14 +2925,9 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Fondiperekond emojide jaoks - - Automatically replies to key requests from other users, if they are verified. - Vasta verifitseeritud kasutajate krรผptovรตtmete pรคringutele automaatselt. - - - + Master signing key - รœldine allkirjavรตti + Allkirjastamise juurvรตti @@ -2402,7 +2945,7 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Teiste kasutajate verifitseerimiseks mรตeldud vรตti. Kui see vรตti on puhverdatud, siis kasutaja verifitseerimine tรคhendab ka kรตikide tema seadmete verifitseerimist. - + Self signing key Omatunnustusvรตti @@ -2432,14 +2975,14 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Kรตik failid (*) - + Open Sessions File Ava sessioonide fail - + @@ -2447,19 +2990,19 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Viga - - + + File Password Faili salasรตna - + Enter the passphrase to decrypt the file: Faili dekrรผptimiseks sisesta salafraas: - + The password cannot be empty Salasรตna ei saa olla tรผhi @@ -2474,6 +3017,14 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Fail, kuhu salvestad eksporditavad sessiooni krรผptovรตtmed + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + รœhtegi krรผptitud vestlust selle kasutajaga ei leidunud. Palun loo temaga krรผptitud vestlus ja proovi uuesti. + + Waiting @@ -2599,37 +3150,6 @@ See tavaliselt tรคhendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Ava kasutaja registreerimise tagavaravariant, lรคbi kรตik sammud ja kinnita seda, kui kรตik valmis on. - - dialogs::JoinRoom - - - Join - Liitu - - - - Cancel - Tรผhista - - - - Room ID or alias - Jututoa tunnus vรตi alias - - - - dialogs::LeaveRoom - - - Cancel - Tรผhista - - - - Are you sure you want to leave? - Kas sa oled kindel, et soovid lahkuda? - - dialogs::Logout @@ -2683,32 +3203,6 @@ Meedia suurus: %2 Vasta reCAPTCHA kรผsimustele ja vajuta kinnita-nuppu - - dialogs::ReadReceipts - - - Read receipts - Lugemisteatised - - - - Close - Sulge - - - - dialogs::ReceiptItem - - - Today %1 - Tรคna %1 - - - - Yesterday %1 - Eile %1 - - message-description sent: @@ -2722,47 +3216,47 @@ Meedia suurus: %2 %1 saatis helifaili - + You sent an image Sa saatsid pildi - + %1 sent an image %1 saatis pildi - + You sent a file Sa saatsid faili - + %1 sent a file %1 saatis faili - + You sent a video Sa saatsid video - + %1 sent a video %1 saatis video - + You sent a sticker Sa saatsid kleepsu - + %1 sent a sticker %1 saatis kleepsu - + You sent a notification Sa saatsid teavituse @@ -2777,7 +3271,7 @@ Meedia suurus: %2 Sina: %1 - + %1: %2 %1: %2 @@ -2797,27 +3291,27 @@ Meedia suurus: %2 Sa helistasid - + %1 placed a call %1 helistas - + You answered a call Sa vastasid kรตnele - + %1 answered a call %1 vastas kรตnele - + You ended a call Sa lรตpetasid kรตne - + %1 ended a call %1 lรตpetas kรตne @@ -2825,7 +3319,7 @@ Meedia suurus: %2 utils - + Unknown Message Type Tundmatu sรตnumitรผรผp diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index 422c2957..2fb5221d 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Soitetaan... @@ -22,7 +22,7 @@ Hide/Show Picture-in-Picture - + Piilota/Nรคytรค kuva kuvassa @@ -45,7 +45,7 @@ Waiting for other side to complete verification. - + Odotetaan toista puolta saamaan vahvistus loppuun. @@ -56,7 +56,7 @@ CallInvite - + Video Call Videopuhelu @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Videopuhelu @@ -117,31 +117,31 @@ CallManager - + Entire screen - + Koko nรคyttรถ ChatPage - + Failed to invite user: %1 - + Kรคyttรคjรครค %1 ei onnistuttu kutsumaan - + Invited user: %1 Kutsuttu kรคyttรคjรค: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Vรคlimuistin tuominen nykyiseen versioon epรคonnistui. Tรคllรค voi olla eri syitรค. Luo vikailmoitus ja yritรค sillรค aikaa kรคyttรครค vanhempaa versiota. Voit myรถs vaihtoehtoisesti koettaa tyhjentรครค vรคlimuistin kรคsin. - + Confirm join Vahvista liittyminen @@ -151,139 +151,141 @@ Haluatko todella liittyรค huoneeseen %1? - + Room %1 created. - + Huone %1 luotu. - + Confirm invite - + Vahvista kutsu - + Do you really want to invite %1 (%2)? - + Haluatko kutsua %1 (%2)? - + Failed to invite %1 to %2: %3 - + Epรคonnistuttiin kutsuminen %1 huoneeseen %2:%3 Confirm kick - + Vahvista potkut Do you really want to kick %1 (%2)? - + Haluatko potkia %1 (%2)? - + Kicked user: %1 - + Potkittiin kรคyttรคjรค: %1 Confirm ban - + Vahvista porttikielto Do you really want to ban %1 (%2)? - + Haluatko antaa porttikiellon kรคyttรคjรคlle %1 (%2)? - + Failed to ban %1 in %2: %3 - + Ei onnistuttu antamaan porttikieltoa kรคyttรคjรคlle %1 huoneessa %2:%3 Banned user: %1 - + Annettiin porttikielto kรคyttรคjรคlle: %1 Confirm unban - + Vahvista porttikiellon purku Do you really want to unban %1 (%2)? - + Haluatko purkaa porttikiellon kรคyttรคjรคltรค %1 (%2)? - + Failed to unban %1 in %2: %3 - + Ei onnistuttu purkamaan porttikieltoa kรคyttรคjรคltรค %1 huoneessa %2: %3 Unbanned user: %1 - + Purettiin porttikielto kรคyttรคjรคltรค %1 - + Do you really want to start a private chat with %1? - + Haluatko luoda yksityisen keskustelun kรคyttรคjรคn %1 kanssa? - + Cache migration failed! - + Vรคlimuistin siirto epรคonnistui! Incompatible cache version - + Yhteensopimaton vรคlimuistin versio The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache. - + Levyllรคsi oleva vรคlimuisti on uudempaa kuin mitรค tรคmรค Nhekon versio tukee. Pรคivitรค tai poista vรคlimuistisi. - + Failed to restore OLM account. Please login again. OLM-tilin palauttaminen epรคonnistui. Ole hyvรค ja kirjaudu sisรครคn uudelleen. + + Failed to restore save data. Please login again. Tallennettujen tietojen palauttaminen epรคonnistui. Ole hyvรค ja kirjaudu sisรครคn uudelleen. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Salausavainten lรคhetys epรคonnistui. Palvelimen vastaus: %1 %2. Ole hyvรค ja yritรค uudelleen myรถhemmin. - - + + Please try to login again: %1 Ole hyvรค ja yritรค kirjautua sisรครคn uudelleen: %1 - + Failed to join room: %1 Huoneeseen liittyminen epรคonnistui: %1 - + You joined the room Sinรค liityit huoneeseen Failed to remove invite: %1 - + Kutsua ei onnistuttu poistamaan: %1 - + Room creation failed: %1 Huoneen luominen epรคonnistui: %1 @@ -293,9 +295,9 @@ Huoneesta poistuminen epรคonnistui: %1 - + Failed to kick %1 from %2: %3 - + Ei onnistuttu potkimaan kรคyttรคjรครค %1 huoneesta %2: %3 @@ -303,7 +305,7 @@ Hide rooms with this tag or from this space by default. - + Piilota huoneet tรคllรค tagilla tai tรคstรค tilasta oletuksena. @@ -316,65 +318,65 @@ Shows all rooms without filtering. - + Nรคytรค kaikki huoneet ilman suodattamista. Favourites - + Suosikit Rooms you have favourited. - + Suosikkihuoneesi. Low Priority - + Matala tรคrkeysjรคrjestys Rooms with low priority. - + Huoneet matalalla tรคrkeysjรคrjestyksellรค. Server Notices - + Palvelimen ilmoitukset Messages from your server or administrator. - + Viestit palvelimeltasi tai yllรคpitรคjรคltรค. CrossSigningSecrets - + Decrypt secrets - + Salaisuuksien salauksen purku Enter your recovery key or passphrase to decrypt your secrets: - + Anna palauttamisavain tai salasana purkaaksesi salaisuuksiesi salaus: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Anna palautusavaimesi tai salasanasi nimeltรค %1 purkaaksesi salaisuuksien salauksen: - + Decryption failed - + Salauksen purku epรคonnistui Failed to decrypt secrets with the provided recovery key or passphrase - + Salaisuuksien salauksen purkaminen ei onnistunut annetulla palautusavaimella tai salasanalla @@ -382,22 +384,22 @@ Verification Code - + Vahvistuskoodi Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification! - + Vahvista seuraavat numerot. Sinun tulisi nรคhdรค samat numerot molemmilla puolilla. Jos niissรค on eroa, paina "Ne eivรคt vastaa toisiaan" peruaksesi vahvistuksen! They do not match! - + Ne eivรคt vastaa toisiaan! They match! - + Ne vastaavat toisiaan! @@ -431,7 +433,7 @@ Hae - + People Ihmiset @@ -476,22 +478,65 @@ Verification Code - + Vahvistuskoodi Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification! - + Vahvista seuraava emoji. Sinun tulisi nรคhdรค sama emoji molemmilla puolilla. Jos ne eroavat toisistaan, paina "Ne eivรคt vastaa toisiaan" peruaksesi vahvistuksen! They do not match! - + Ne eivรคt vastaa toisiaan! They match! - + Ne vastaavat toisiaan! + + + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Tรคmรคn viestin avaamista varten ei ole avainta. Pyysimme avainta automaattisesti, mutta voit yrittรครค pyytรครค sitรค uudestaan jos olet kรคrsimรคtรถn. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Tรคmรคn viestin salausta ei voitu purkaa, koska meillรค on avain vain uudemmille viesteille. Voit yrittรครค pyytรครค pรครคsyรค tรคhรคn viestiin. + + + + There was an internal error reading the decryption key from the database. + Sisรคinen virhe tapahtui kun salausavainta yritettiin lukea tietokannasta. + + + + There was an error decrypting this message. + Tรคmรคn viestin salauksen purkamisessa tapahtui virhe. + + + + The message couldn't be parsed. + Tรคtรค viestiรค ei voitu jรคsentรครค. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + Salausavainta kรคytettiin uudelleen! Joku yrittรครค mahdollisesti tuoda vรครคriรค viestejรค tรคhรคn keskusteluun! + + + + Unknown decryption error + Tuntematon virhe salauksen purkamisessa + + + + Request key + Pyydรค avainta @@ -504,62 +549,17 @@ Encrypted by a verified device - + Vahvistetun laitteen salaama Encrypted by an unverified device, but you have trusted that user so far. - + Vahvistamattoman laitteen salama, mutta olet luottanut tรคhรคn asti tuohon kรคyttรคjรครคn. - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Salattu viesti (salauksen purkuavaimia ei lรถydetty) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- Virhe purkaessa salausta (megolm-avaimien hakeminen tietokannasta epรคonnistui) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Virhe purkaessa salausta (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Salattu viesti (tuntematon viestityyppi) -- - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- - + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Vahvistamattoman laitteen salaama tai tรคmรค avain on epรคluotettavasta lรคhteestรค kuten avaimen varmuuskopiosta. @@ -567,31 +567,40 @@ Verification failed - + Vahvistus epรคonnistui Other client does not support our verification protocol. - + Toinen asiakasohjelma ei tue vahvistusprotokollaamme. Key mismatch detected! + Tunnistettiin virheellinen avain! + + + + Device verification timed out. + Aikakatkaisu laitteen vahvistuksessa. + + + + Other party canceled the verification. + Toinen osapuoli perui vahvistuksen. + + + + Verification messages received out of order! - - Device verification timed out. + Unknown verification error. - - Other party canceled the verification. - - - - + Close Sulje @@ -604,48 +613,138 @@ Vรคlitรค viesti + + ImagePackEditorDialog + + + Editing image pack + Muokataan kuvapakkausta + + + + Add images + Lisรครค kuvia + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Tarrat (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + TIla-avain + + + + Packname + Pakkauksen nimi + + + + Attribution + Osoitus + + + + + Use as Emoji + Kรคytรค emojina + + + + + Use as Sticker + Kรคytรค tarrana + + + + Shortcode + Lyhyt koodi + + + + Body + Runko + + + + Remove from pack + Poista pakkauksesta + + + + Remove + Poista + + + + Cancel + Peruuta + + + + Save + Tallenna + + ImagePackSettingsDialog Image pack settings - + Kuvapakkauksen asetukset - + + Create account pack + Luo tilipakkaus + + + + New room pack + Uusi huonepakkaus + + + Private pack - + Yksityinen pakkaus Pack from this room - + Pakkaus tรคlle huoneelle Globally enabled pack - + Kaikkialla kรคytรถssรค oleva pakkaus - + Enable globally - + Salli kรคytettรคvรคksi kaikkialla Enables this pack to be used in all rooms - + Sallii tรคmรคn pakkauksen kรคytettรคvรคksi kaikissa huoneissa - + + Edit + Muokkaa + + + Close - Sulje + Sulje InputBar - + Select a file Valitse tiedosto @@ -655,43 +754,69 @@ Kaikki Tiedostot (*) - + Failed to upload media. Please try again. - + Mediaa ei onnistuttu lataamaan. Yritรค uudelleen. InviteDialog - + Invite users to %1 - + Kutsu kรคyttรคjiรค %1 User ID to invite - Kรคyttรคjรคtunnus kutsuttavaksi + Kรคyttรคjรคtunnus kutsuttavaksi @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @matti:matrix.org Add - + Lisรครค Invite - + Kutsu Cancel - Peruuta + Peruuta + + + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + Huoneen tunnus tai osoite + + + + LeaveRoomDialog + + + Leave room + Poistu huoneesta + + + + Are you sure you want to leave? + Oletko varma, ettรค haluat poistua? @@ -712,7 +837,10 @@ You can also put your homeserver address there, if your server doesn't support .well-known lookup. Example: @user:server.my If Nheko fails to discover your homeserver, it will show you a field to enter the server manually. - + Kirjautumisnimesi. MXID:n pitรคisi alkaa @ -merkillรค, jota seuraa kรคyttรคjรคtunnus. Kรคyttรคjรคtunnuksen jรคlkeen sinun pitรครค antaa palvelimen nimi kaksoispisteen (:) jรคlkeen. +Voit myรถs laittaa tรคhรคn kotipalvelimesi osoitteen, jos palvelimesi ei tunne etsintรครค. +Esimerkki: @user:server.my +Jos Nheko ei onnistu lรถytรคmรครคn kotipalvelintasi, se nรคyttรครค sinulle kentรคn, johon laittaa palvelin kรคsin. @@ -732,7 +860,7 @@ If Nheko fails to discover your homeserver, it will show you a field to enter th A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used. - + Tรคmรคn laitteen nimi, joka nรคytetรครคn muille kun laitteitasi vahvistetaan. Oletusta kรคytetรครคn jos mitรครคn ei anneta. @@ -742,13 +870,14 @@ If Nheko fails to discover your homeserver, it will show you a field to enter th server.my:8787 - + server.my:8787 The address that can be used to contact you homeservers client API. Example: https://server.my:8787 - + Osoite, jota voidaan kรคyttรครค ottamaan yhteyttรค kotipalvelimesi asiakasrajapintaan. +Esimerkki: https://server.my:8787 @@ -756,25 +885,25 @@ Example: https://server.my:8787 KIRJAUDU - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Vรครคrรค Matrix-tunnus. Esim. @joe:matrix.org - + Autodiscovery failed. Received malformed response. Palvelimen tietojen hakeminen epรคonnistui: virheellinen vastaus. - + Autodiscovery failed. Unknown error when requesting .well-known. Palvelimen tietojen hakeminen epรคonnistui: tuntematon virhe hakiessa .well-known -tiedostoa. - + The required endpoints were not found. Possibly not a Matrix server. Vaadittuja pรครคtepisteitรค ei lรถydetty. Mahdollisesti ei Matrix-palvelin. @@ -784,33 +913,51 @@ Example: https://server.my:8787 Vastaanotettiin virheellinen vastaus. Varmista, ettรค kotipalvelimen osoite on pรคtevรค. - + An unknown error occured. Make sure the homeserver domain is valid. Tapahtui tuntematon virhe. Varmista, ettรค kotipalvelimen osoite on pรคtevรค. - + SSO LOGIN - + SSO-kirjautuminen - + Empty password Tyhjรค salasana - + SSO login failed + SSO-kirjautuminen epรคonnistui + + + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? MessageDelegate - + removed - + poistettu @@ -818,44 +965,44 @@ Example: https://server.my:8787 Salaus on kรคytรถssรค - + room name changed to: %1 huoneen nimi muutettu: %1 removed room name - + poistettu huoneen nimi topic changed to: %1 - + aihe vaihdettiin: %1 removed topic - + poistettu aihe %1 changed the room avatar - + %1 muutti huoneen avataria %1 created and configured room: %2 - + %1 loi ja sรครคti huoneen: %2 %1 placed a voice call. - + %1 asetti รครคnipuhelun. %1 placed a video call. - + %1 laittoi videopuhelun. @@ -870,12 +1017,17 @@ Example: https://server.my:8787 %1 ended the call. - + %1 pรครคtti puhelun. Negotiating call... - + Neuvotellaan puhelua.โ€ฆ + + + + Allow them in + Salli heidรคt sisรครคn @@ -883,12 +1035,12 @@ Example: https://server.my:8787 Hang up - + Punainen luuri Place a call - + Soita puhelu @@ -901,9 +1053,9 @@ Example: https://server.my:8787 Kirjoita viestiโ€ฆ - + Stickers - + Tarrat @@ -913,18 +1065,18 @@ Example: https://server.my:8787 Send - + Lรคhetรค You don't have permission to send messages in this room - + Sinulla ei ole lupaa lรคhettรครค viestejรค tรคssรค huoneessa MessageView - + Edit Muokkaa @@ -944,74 +1096,81 @@ Example: https://server.my:8787 Asetukset - + + &Copy - + &Kopioi - + + Copy &link location - + Kopioi &linkki sijainti - + Re&act - + Rea&goi Repl&y - + Vast&aa &Edit - + &Muokkaa Read receip&ts - + Lue kuitt&eja &Forward - + &Lรคhetรค eteenpรคin &Mark as read - + &Merkitse luetuksi View raw message - + Nรคytรค sisรคltรถ raakamuodossa View decrypted raw message - + Nรคytรค salaukseltaan purettu raaka viesti Remo&ve message - + Poist&a viesti &Save as - + &Tallenna nimellรค &Open in external program - + &Avaa ulkoisessa sovelluksessa Copy link to eve&nt - + Kopioi linkki tapaht&umaan + + + + &Go to quoted message + &Mene lainattuun viestiin @@ -1019,37 +1178,42 @@ Example: https://server.my:8787 Send Verification Request - + Lรคhetรค vahvistuspyyntรถ Received Verification Request + Otettiin vastaan vahvistuspyyntรถ + + + + 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.) - + 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? - + Voit vahvistaa laitteesi, jotta sallit muiden nรคhdรค, mitkรค niistรค oikeasti kuuluvat sinulle. Tรคmรค myรถs mahdollistaa avaimen varmuuskopioinnin toiminnnan automaattisesti. Vahvistetaanko %1 nyt? To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party. - + Varmistaaksesi, ettei kukaan pahantahtoinen kรคyttรคjรค voi salakuunnella salattuja keskustelujanne, voit vahvistaa toisen osapuolen. %1 has requested to verify their device %2. - + %1 on pyytรคnyt vahvistamaan hรคnen laitteeensa %2. %1 using the device %2 has requested to be verified. - + %1 kรคyttรครค laitetta, jonka %2 on pyytรคnyt vahvistamaan. Your device (%1) has requested to be verified. - + Laitteesi (%1) on pyytรคnyt vahvistetuksi tulemista. @@ -1059,12 +1223,12 @@ Example: https://server.my:8787 Deny - + Kiellรค Start verification - + Aloita vahvistus @@ -1072,33 +1236,29 @@ Example: https://server.my:8787 Hyvรคksy + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 lรคhetti salatun viestin - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message %1 vastasi: %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1121,7 +1281,7 @@ Example: https://server.my:8787 Place a call to %1? - + Soita henkilรถlle %1? @@ -1129,19 +1289,19 @@ Example: https://server.my:8787 Mikrofonia ei lรถydy. - + Voice - + ร„รคni Video - + Video Screen - + Nรคyttรถ @@ -1154,49 +1314,65 @@ Example: https://server.my:8787 unimplemented event: - + toistaseksi toteuttamaton tapahtuma: QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. - + Luo uniikki profili, joka mahdollistaa kirjautumisen usealle tilille samanaikaisesti ja useamman nheko-instanssin aloittamisen. profile - + profiili profile name - + profiilin nimi + + + + ReadReceipts + + + Read receipts + Lukukuittaukset + + + + ReadReceiptsModel + + + Yesterday, %1 + Eilen, &1 RegisterPage - + Username Kรคyttรคjรคnimi - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. Kรคyttรคjรคtunnus ei saa olla tyhjรค, ja se saa sisรคltรครค vain merkkejรค a-z, 0-9, ., _, =, - ja /. - + Password Salasana Please choose a secure password. The exact requirements for password strength may depend on your server. - + Valitse turvallinen salasana. Tarkat vaatimukset salasanan vahvuudelle voivat riippua palvelimestasi. @@ -1209,9 +1385,9 @@ Example: https://server.my:8787 Kotipalvelin - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. - + Palvelin, joka sallii rekisterรถinnin. Koska matrix on hajautettu, sinun pitรครค ensin lรถytรครค palvelin jolle rekisterรถityรค tai yllรคpitรครค omaasi. @@ -1219,27 +1395,17 @@ Example: https://server.my:8787 REKISTERร–IDY - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. Palvelimen tietojen hakeminen epรคonnistui: virheellinen vastaus. - + Autodiscovery failed. Unknown error when requesting .well-known. Palvelimen tietojen hakeminen epรคonnistui: tuntematon virhe hakiessa .well-known -tiedostoa. - + The required endpoints were not found. Possibly not a Matrix server. Vaadittuja pรครคtepisteitรค ei lรถydetty. Mahdollisesti ei Matrix-palvelin. @@ -1254,17 +1420,17 @@ Example: https://server.my:8787 Tapahtui tuntematon virhe. Varmista, ettรค kotipalvelimen osoite on pรคtevรค. - + Password is not long enough (min 8 chars) Salasana ei ole tarpeeksi pitkรค (vรคhintรครคn 8 merkkiรค) - + Passwords don't match Salasanat eivรคt tรคsmรครค - + Invalid server name Epรคkelpo palvelimen nimi @@ -1272,7 +1438,7 @@ Example: https://server.my:8787 ReplyPopup - + Close Sulje @@ -1282,10 +1448,28 @@ Example: https://server.my:8787 Peruuta muokkaus + + RoomDirectory + + + Explore Public Rooms + Tutki julkisia huoneita + + + + Search for public rooms + Etsi julkisia huoneita + + + + Choose custom homeserver + + + RoomInfo - + no version stored ei tallennettua versiota @@ -1293,24 +1477,14 @@ Example: https://server.my:8787 RoomList - + New tag - + Uusi tagi Enter the tag you want to use: - - - - - Leave Room - - - - - Are you sure you want to leave this room? - + Kirjoita tagi jota haluat kรคyttรครค: @@ -1320,55 +1494,78 @@ Example: https://server.my:8787 Tag room as: - + Laita huoneelle tagi: Favourite - + Suosikki Low priority - + Matala tรคrkeysjรคrjestys Server notice - + Palvelimen ilmoitus Create new tag... - + Luo uusi tagi... - + Status Message - + Tilapรคivitys Enter your status message: - + Kirjoita tilapรคivityksesi: Profile settings - + Profiilin asetukset Set status message - + Aseta tilapรคivitys - + Logout Kirjaudu ulos - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Sulje + + + Start a new chat Aloita uusi keskustelu @@ -1380,7 +1577,7 @@ Example: https://server.my:8787 Create a new room - + Luo uusi huone @@ -1388,7 +1585,7 @@ Example: https://server.my:8787 Huoneluettelo - + User settings Kรคyttรคjรคasetukset @@ -1396,79 +1593,114 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + &1 jรคsenet - + %n people in %1 Summary above list of members - - - + + %n henkilรถ huoneessa %1 + %n henkilรถรค huonessa %1 Invite more people - + Kutsu lisรครค ihmisiรค + + + + This room is not encrypted! + Tรคmรค huone ei ole salattu! + + + + This user is verified. + Tรคmรค kรคyttรคjรค on vahvistettu. + + + + This user isn't verified, but is still using the same master key from the first time you met. + Tรคtรค kรคyttรคjรครค ei ole vahvistettu, mutta hรคn kรคyttรครค edelleen samaa pรครคvavainta kuin ensimmรคisellรค tapaamiskerralla. + + + + This user has unverified devices! + Tรคllรค kรคyttรคjรคllรค on vahvistamattomia laitteita! RoomSettings - + Room Settings - + Huoneen asetukset - + %1 member(s) - + %1 jรคsentรค SETTINGS - + ASETUKSET Notifications - + Ilmoitukset Muted - + Mykistetty Mentions only - + Vain maininnat All messages - + Kaikki viestit - + + Room access + Huoneeseen pรครคsy + + + Anyone and guests - + Kaikki ja vieraat Anyone - + Kuka tahansa Invited users - + Kutsutut kรคyttรคjรคt - + + By knocking + Koputtamalla + + + + Restricted by membership in other rooms + Rajoitettu jรคsenyyden perusteella muissa huoneissa + + + Encryption Salaus @@ -1481,32 +1713,32 @@ Example: https://server.my:8787 Encryption is currently experimental and things might break unexpectedly. <br> Please take note that it can't be disabled afterwards. - + Salaus on tรคllรค hetkellรค kokeellinen ja asiat voivat mennรค rikki odottamattomasti.<br>Huomaa ettรค sitรค ei voi poistaa jรคlkikรคteen. Sticker & Emote Settings - + Tarra- ja emojiasetukset Change - + Muuta Change what packs are enabled, remove packs or create new ones - + Muuta mitkรค pakkaukset ovat sallittuja, poista pakkauksia tai luo uusia INFO - + TIETOA Internal ID - + Sisรคinen ID @@ -1514,12 +1746,12 @@ Example: https://server.my:8787 Huoneen versio - + Failed to enable encryption: %1 Salauksen aktivointi epรคonnistui: %1 - + Select an avatar Valitse profiilikuva @@ -1539,8 +1771,8 @@ Example: https://server.my:8787 Virhe lukiessa tiedostoa: %1 - - + + Failed to upload image: %s Kuvan lรคhetys epรคonnistui: %s @@ -1548,18 +1780,46 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Kutsua odotetaan. - + Previewing this room + Esikatsellaan tรคtรค huonetta + + + + No preview available + Esikatselu ei saatavilla + + + + Root + + + Please enter your login password to continue: - - No preview available + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. @@ -1568,33 +1828,33 @@ Example: https://server.my:8787 Share desktop with %1? - + Jaa tyรถpรถytรค kรคyttรคjรคn %1 kanssa? Window: - + Ikkuna: Frame rate: - + Ruudunpรคivitys: Include your camera picture-in-picture - + Sisรคllytรค kamerasi kuva kuvassa -tilaan Request remote camera - + Pyydรค etรคkameraa View your callee's camera like a regular video call - + Nรคytรค puhelun vastaanottajan kamera tavallisen videopuhelun tapaan @@ -1604,12 +1864,12 @@ Example: https://server.my:8787 Share - + Jaa Preview - + Esikatsele @@ -1617,27 +1877,142 @@ Example: https://server.my:8787 Peruuta + + SecretStorage + + + Failed to connect to secret storage + Salattuun tallennustilaan ei saatu yhteyttรค + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Kuvapakkausta %1 ei onnistuttu pรคivittรคmรครคn + + + + Failed to delete old image pack: %1 + Vanhaa kuvapakkausta %1 ei onnistuttu poistamaan + + + + Failed to open image: %1 + Kuvaa %1 ei onnistuttu avaamaan + + + + Failed to upload image: %1 + Kuvan lรคhetys epรคonnistui: %s + + StatusIndicator Failed - + Epรคonnnistui Sent - + Lรคhetetyt Received - + Vastaanotetut Read - + Lue @@ -1645,7 +2020,7 @@ Example: https://server.my:8787 Search - Hae + Hae @@ -1653,12 +2028,12 @@ Example: https://server.my:8787 Successful Verification - + Onnistunut varmistus Verification successful! Both sides verified their devices! - + Varmistus onnistui! Molemmat osapuolet vahvistivat laitteensa! @@ -1669,18 +2044,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Viestin muokkaus epรคonnistui: %1 - + Failed to encrypt event, sending aborted! - + Tapahtuman salaus epรคonnistui, lรคhetys keskeytetรครคn! - + Save image Tallenna kuva @@ -1700,7 +2075,7 @@ Example: https://server.my:8787 Tallenna tiedosto - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1709,79 +2084,94 @@ Example: https://server.my:8787 - + %1 opened the room to the public. - + %1 avasi huoneen julkiseksi. %1 made this room require and invitation to join. - + %1 teki tรคstรค huoneesta liittymiskutsun vaativan. - + + %1 allowed to join this room by knocking. + Kรคyttรคjรคn %1 annettiin liittyรค tรคhรคn huoneeseen koputtamalla. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 salli seuraavien huoneiden jรคsenten liittyรค automaattisesti tรคhรคn huoneeseen: %2 + + + %1 made the room open to guests. - + %1 teki huoneesta avoimen vieraille. %1 has closed the room to guest access. - + %1 on sulkenut huoneen vierailta. %1 made the room history world readable. Events may be now read by non-joined people. - + %1 teki huoneen historian luettavaksi kaikille. Tapahtumia voivat nyt lukea myรถs huoneeseen liittymรคttรถmรคt ihmiset. %1 set the room history visible to members from this point on. - + %1 asetti huoneen historian nรคkyvรคksi jรคsenille tรคstรค lรคhtien. - + %1 set the room history visible to members since they were invited. - + %1 asetti huoneen historian nรคkyvรคksi jรคsenille kutsumisesta lรคhtien. - + %1 set the room history visible to members since they joined the room. - + %1 asetti huoneen historian nรคkyvรคksi jรคsenille huoneeseen liittymisen jรคlkeen. %1 has changed the room's permissions. - + %1 on muuttanut huoneen lupia. - + %1 was invited. - + &1 kutsuttiin. - + %1 changed their avatar. - + %1 muutti avatariaan. %1 changed some profile info. - + %1 muutti joitain tietoja profiilistaan. - + %1 joined. %1 liittyi. - + + %1 joined via authorisation from %2's server. + %1 liittyi kรคyttรคjรคn %2 palvelimen suomalla vahvistuksella. + + + %1 rejected their invite. - + %1 hylkรคsi kutsunsa. Revoked the invite to %1. - + Peruttiin kutsu kรคyttรคjรคlle %1. @@ -1791,64 +2181,64 @@ Example: https://server.my:8787 Kicked %1. - + Potkittiin %1. Unbanned %1. - + Poistettiin kรคyttรคjรคn %1 porttikielto. %1 was banned. - + Kรคyttรคjรคlle %1 annettiin porttikielto. - + Reason: %1 - + Syy: %1 - + %1 redacted their knock. - + %1 perui koputuksensa. - + You joined this room. Sinรค liityit tรคhรคn huoneeseen. - + %1 has changed their avatar and changed their display name to %2. - + %1 vaihtoi avatariaan ja vaihtoi nรคyttรถnimekseen %2. - + %1 has changed their display name to %2. - + %1 vaihtoi nรคyttรถnimekseen %2. - + Rejected the knock from %1. - + Hylรคttiin koputus kรคyttรคjรคltรค %1. %1 left after having already left! This is a leave event after the user already left and shouldn't happen apart from state resets - + %1 lรคhti vaikka lรคhti jo aiemmin! %1 knocked. - + %1 koputti. TimelineRow - + Edited Muokattu @@ -1856,58 +2246,75 @@ Example: https://server.my:8787 TimelineView - + No room open - + Ei avointa huonetta - + + No preview available + Esikatselu ei saatavilla + + + %1 member(s) - + %1 jรคsentรค join the conversation - + liity keskusteluun accept invite - + salli kutsu decline invite - + peru kutsu Back to room list - - - - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - + Takaisin huonelistaan TopBar - + Back to room list - + Takaisin huonelistaan - + No room selected + Ei valittua huonetta + + + + This room is not encrypted! + Tรคmรค huone ei ole salattu! + + + + This room contains only verified devices. + Tรคmรค huone sisรคltรครค vain vahvistettuja laitteita. + + + + This room contains verified devices and devices which have never changed their master key. - + + This room contains unverified devices! + Tรคmรค huone sisรคltรครค varmentamattomia laitteita! + + + Room options Huoneen asetukset @@ -1945,46 +2352,136 @@ Example: https://server.my:8787 Lopeta + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Anna kelvollinen rekisterรถitymispoletti. + + + + Invalid token + + + UserProfile - + Global User Profile - + Yleinen kรคyttรคjรคprofiili Room User Profile - + Huoneen kรคyttรคjรคprofiili - - + + Change avatar globally. + Vaihda avataria kaikkialla. + + + + Change avatar. Will only apply to this room. + Muuta avataria. Toimii vain tรคssรค huoneessa. + + + + Change display name globally. + Muuta nรคyttรถnimeรค kaikkialla. + + + + Change display name. Will only apply to this room. + Muuta nรคyttรถnimeรค. Toimii vain tรคssรค huoneessa. + + + + Room: %1 + Huone: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + Tรคmรค on huoneelle erityinen profiili. Kรคyttรคjรคn nimi ja avatar voivat erota niiden kaikkialla kรคytรถssรค olevista versioista. + + + + Open the global profile for this user. + Avaa tรคmรคn kรคyttรคjรคn yleinen profiili. + + + + Verify - + Vahvista - - Ban the user - - - - - Start a private chat - + + Start a private chat. + Aloita yksityinen keskustelu. - Kick the user + Kick the user. + Potki kรคyttรคjรค. + + + + Ban the user. + Anna kรคyttรคjรคlle porttikielto. + + + + Refresh device list. - + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify + Peru vahvistus + + + + Sign out device %1 - + + You signed out this device. + + + + Select an avatar Valitse profiilikuva @@ -2007,16 +2504,16 @@ Example: https://server.my:8787 UserSettings - - + + Default - + Oletus UserSettingsPage - + Minimize to tray Pienennรค ilmoitusalueelle @@ -2026,119 +2523,134 @@ Example: https://server.my:8787 Aloita ilmoitusalueella - + Group's sidebar Ryhmรคsivupalkki - + Circular Avatars - + Pyรถreรคt avatarit - + profile: %1 - + profiili: %1 - + Default - + Oletus CALLS - + PUHELUT Cross Signing Keys - + Ristiin allekirjoitetut avaimet REQUEST - + PYYNTร– DOWNLOAD - + LATAA - + Keep the application running in the background after closing the client window. - + Anna sovelluksen pyรถriรค taustalla asiakasohjelman ikkunan sulkemisen jรคlkeen. Start the application in the background without showing the client window. - + Aloita sovellus taustalla nรคyttรคmรคttรค asiakasohjelman ikkunaa. Change the appearance of user avatars in chats. OFF - square, ON - Circle. + Muuta kรคyttรคjien avatarien ulkonรคkรถรค keskusteluissa. +POIS Pร„ร„LTร„ - neliรถ, Pร„ร„LLร„ - pyรถreรค. + + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. Show a column containing groups and tags next to the room list. - + Nรคytรค huonelistan vieressรค tagit ja ryhmรคt sisรคltรคvรค sarake. Decrypt messages in sidebar - + Pura viestien salaus sivupalkissa Decrypt the messages shown in the sidebar. Only affects messages in encrypted chats. - + Pura sivupalkissa nรคkyvien viestien salaus +Vaikuttaa vain salattujen keskustelujen viesteihin. Privacy Screen - + Yksityisyysnรคkymรค When the window loses focus, the timeline will be blurred. - + Kun ikkuna ei ole kohdistettuna, tรคmรค aikajana +sumennetaan. - + Privacy screen timeout (in seconds [0 - 3600]) - + Yksityisyysnรคkymรคn aikakatkaisu (sekunneissa [0-3600]) Set timeout (in seconds) for how long after window loses focus before the screen will be blurred. Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) - + Aseta aikakatkaisu (sekunneissa) ikkunan kohdistuksen kadottamiselle +ennen kuin nรคkymรค sumennetaan. +Aseta nollaan, jotta sumennetaan heti kohdistus kadotetaan. Suurin arvo 1 tunti (3600 sekuntia) Show buttons in timeline - + Nรคytรค painikkeet aikajanalla Show buttons to quickly reply, react or access additional options next to each message. - + Nรคytรค painikkeet vastataksesi nopeasti, reagoidaksesi tai pรครคstรคksesi lisรคtoimintoihin joka viestin vieressรค. Limit width of timeline - + Rajoita aikajanan leveyttรค Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised - + Aseta viestien suurin leveys aikajanalla (pikseleissรค). Tรคmรค voi auttaa luettavuutta laajakuvassa, kun Nheko on tรคyden ruudun tilassa. @@ -2149,22 +2661,25 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds Show who is typing in a room. This will also enable or disable sending typing notifications to others. - + Nรคytรค kuka kirjoittaa huoneessa. +Tรคmรค myรถs sallii tai evรครค kirjoitusilmoitusten lรคhettรคmisen muille. Sort rooms by unreads - + Lajittele huoneet lukemattomien mukaan Display rooms with new messages first. If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room. If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. - + Nรคytรค ensiksi huoneet, joissa on uusia viestejรค. +Jos tรคmรค on poissa pรครคltรค, lista huoneista lajitellaan vain huoneen viimeisimmรคn viestin aikaleiman mukaan. +Jos tรคmรค on pรครคllรค, huoneet, joissa ilmoitukset ovat pรครคllรค (pieni ympyrรค, jonka sisรคssรค on numero), lajitellaan pรครคllimmรคisiksi. Mykistรคmรคsi huoneet lajitellaan aikaleiman mukaan, koska et nรคhtรคvรคsti pidรค niitรค yhtรค tรคrkeinรค kuin muita huoneita. - + Read receipts Lukukuittaukset @@ -2172,84 +2687,127 @@ If this is on, rooms which have active notifications (the small circle with a nu Show if your message was read. Status is displayed next to timestamps. - + Nรคytรค jos viestisi oli luettu. +Tila nรคytetรครคn aikaleimojen vieressรค. - + Send messages as Markdown - + Lรคhetรค viestit Markdownina Allow using markdown in messages. When disabled, all messages are sent as a plain text. - + Salli Markdownin kรคyttรถ viesteissรค. +Kun poissa pรครคltรค, kaikki viestit lรคhetetรครคn tavallisena tekstinรค. + Play animated images only on hover + Toista animoidut kuvat vain kun kohdistin on niiden pรครคllรค + + + Desktop notifications Tyรถpรถytรคilmoitukset Notify about received message when the client is not currently focused. - + Ilmoita vastaanotetusta viestistรค kun ohjelma ei ole kohdistettuna. Alert on notification - + Hรคlytรค ilmoituksesta Show an alert when a message is received. This usually causes the application icon in the task bar to animate in some fashion. - + Nรคytรค hรคlytys kun viesti on vastaanotettu. +Tรคmรค yleensรค saa sovelluksen kuvakkeen liikkumaan jollain tapaa tehtรคvรคpalkissa. Highlight message on hover - + Korosta viestiรค kun kohdistin on pรครคllรค Change the background color of messages when you hover over them. - + Muuta viestien taustavรคriรค kun kohdistimesi liikkuu niiden yli. Large Emoji in timeline - + Iso emoji aikajanalla Make font size larger if messages with only a few emojis are displayed. - + Suurenna fonttikokoa jos nรคytetรครคn viestit vain muutamalla emojilla. - + + Send encrypted messages to verified users only + Lรคhetรค salatut viestit vain vahvistetuille kรคyttรคjille + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Vaatii kรคyttรคjรคn olevan vahvistettu, jotta hรคnelle voi lรคhettรครค salattuja viestejรค. Tรคmรค parantaa turvallisuutta, mutta tekee pรครคstรค-pรครคhรคn -salauksen hankalammaksi. + + + Share keys with verified users and devices + Jaa avaimet vahvistettujen kรคyttรคjien ja laitteiden kanssa + + + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Automaattisesti vastaa avainpyyntรถihin, jos ne ovat vahvistettuja, vaikka tuolla laitteella ei tulisi muuten olla pรครคsyรค noihin avaimiin. + + + + Online Key Backup + Avaimen varmuuskopiointi verkkoon + + + + Download message encryption keys from and upload to the encrypted online key backup. - + + Enable online key backup + Salli avaimen varmuuskopiointi verkkoon + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + Nhekon tekijรคt eivรคt suosittele avaimen varmuuskopiointia verkkoon kunnes avaimen symmetrinen varmuuskopiointi verkkoon on saatavilla. Sallitaanko se kuitenkin? + + + CACHED - + Vร„LIMUISTISSA NOT CACHED - + EI Vร„LIMUISTISSA - + Scale factor Mittakerroin Change the scale factor of the whole user interface. - + Muuta koko kรคyttรถliittymรคn kokoa. @@ -2274,7 +2832,7 @@ This usually causes the application icon in the task bar to animate in some fash Set the notification sound to play when a call invite arrives - + Aseta ilmoitusรครคni soimaan kun kutsu puheluun tulee @@ -2289,22 +2847,22 @@ This usually causes the application icon in the task bar to animate in some fash Camera resolution - + Kameran resoluutio Camera frame rate - + Kameran ruudunpรคivitys Allow fallback call assist server - + Salli varajรคrjestelynรค toimiva puhelua avustava palvelin Will use turn.matrix.org as assist when your home server does not offer one. - + Kรคyttรครค apuna palvelinta turn.matrix.org silloin kun kotipalvelimesi ei sellaista tarjoa. @@ -2317,7 +2875,7 @@ This usually causes the application icon in the task bar to animate in some fash Laitteen sormenjรคlki - + Session Keys Istunnon avaimet @@ -2337,74 +2895,74 @@ This usually causes the application icon in the task bar to animate in some fash SALAUS - + GENERAL YLEISET ASETUKSET - + INTERFACE - + Kร„YTTร–LIITTYMร„ - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Soittaa mediaa kuten GIF- ja WEBP-tiedostoja vain kun kursori on niiden kohdalla. + + + Touchscreen mode - + Kosketusnรคyttรถtila Will prevent text selection in the timeline to make touch scrolling easier. - + Estรครค tekstin valitsemisen aikajanalla, jotta koskettamalla vierittรคminen on helpompaa. Emoji Font Family - + Emojien fonttiperhe - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key - + Pรครคtason allekirjoittava avain Your most important key. You don't need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys. - + Kaikkein tรคrkein avaimesi. Sinun ei tarvitse laittaa sitรค vรคlimuistiin, koska silloin sen varastaminen on epรคtodennรคkรถistรค ja sitรค vaaditaan vain kierrรคttรคmรครคn muita allekirjoittavia avaimiasi. User signing key - + Kรคyttรคjรคn allekirjoittava avain The key to verify other users. If it is cached, verifying a user will verify all their devices. - + Avain vahvistamaan muita kรคyttรคjiรค. Jos se on vรคlimuistissa, kรคyttรคjรคn varmistaminen varmistaa hรคnen kaikki laitteensa. - + Self signing key - + Itsensรค allekirjoittava avain The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you. - + Avain vahvistamaan omat avaimesi. Jos se on vรคlimuistisas, yhden laitteesi vahvistaminen laittaa sen vahvistetuksi kaikille muille laitteillesi ja kรคyttรคjille, jotka ovat vahvistaneet sinut. Backup key - + Varmuuskopioavain The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server. - + Avain purkamaan avainten varmuuskopioita verkossa. Jos se laitetaan vรคlimuistiin, voit sallia avainten varmuuskopioinnin verkossa sรคilรถรคksesi salausavaimet, jotka ovat turvallisesti salattuja palvelimella. @@ -2417,14 +2975,14 @@ This usually causes the application icon in the task bar to animate in some fash Kaikki Tiedostot (*) - + Open Sessions File Avaa Istuntoavaintiedosto - + @@ -2432,19 +2990,19 @@ This usually causes the application icon in the task bar to animate in some fash Virhe - - + + File Password Tiedoston Salasana - + Enter the passphrase to decrypt the file: Anna salasana tiedoston salauksen purkamiseksi: - + The password cannot be empty Salasana ei voi olla tyhjรค @@ -2459,27 +3017,35 @@ This usually causes the application icon in the task bar to animate in some fash Tiedosto, johon viedyt istuntoavaimet tallennetaan + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Salattua keskustelua ei lรถydetty tรคlle kรคyttรคjรคlle. Luo salattu yksityiskeskustelu tรคmรคn kรคyttรคjรคn kanssa ja yritรค uudestaan. + + Waiting Waiting for other partyโ€ฆ - + Odotetaan toista osapuoltaโ€ฆ Waiting for other side to accept the verification request. - + Odotetaan toista osapuolta hyvรคksymรครคn vahvistuspyyntรถ. Waiting for other side to continue the verification process. - + Odotetaan toista puolta jatkamaan vahvistusta. Waiting for other side to complete the verification process. - + Odotetaan toista puolta saamaan vahvistus valmiiksi. @@ -2497,7 +3063,7 @@ This usually causes the application icon in the task bar to animate in some fash Enjoy your stay! - + Nauti vierailustasi! @@ -2566,7 +3132,7 @@ This usually causes the application icon in the task bar to animate in some fash Open Fallback in Browser - + Avaa varajรคrjestely selaimessa @@ -2581,38 +3147,7 @@ This usually causes the application icon in the task bar to animate in some fash Open the fallback, follow the steps and confirm after completing them. - - - - - dialogs::JoinRoom - - - Join - Liity - - - - Cancel - Peruuta - - - - Room ID or alias - Huoneen tunnus tai osoite - - - - dialogs::LeaveRoom - - - Cancel - Peruuta - - - - Are you sure you want to leave? - Oletko varma, ettรค haluat poistua? + Avaa varajรคrjestely, seuraa ohjeita ja vahvista kun olet saanut ne valmiiksi. @@ -2668,32 +3203,6 @@ Median koko: %2 Ratkaise reCAPTCHA ja paina varmista-nappia - - dialogs::ReadReceipts - - - Read receipts - Lukukuittaukset - - - - Close - Sulje - - - - dialogs::ReceiptItem - - - Today %1 - Tรคnรครคn %1 - - - - Yesterday %1 - Eilen %1 - - message-description sent: @@ -2707,47 +3216,47 @@ Median koko: %2 %1 lรคhetti รครคnileikkeen - + You sent an image Lรคhetit kuvan - + %1 sent an image %1 lรคhetti kuvan - + You sent a file Lรคhetit tiedoston - + %1 sent a file %1 lรคhetti tiedoston - + You sent a video Lรคhetit videotiedoston - + %1 sent a video %1 lรคhetti videotiedoston - + You sent a sticker Lรคhetit tarran - + %1 sent a sticker %1 lรคhetti tarran - + You sent a notification Lรคhetit ilmoituksen @@ -2762,7 +3271,7 @@ Median koko: %2 Sinรค: %1 - + %1: %2 %1: %2 @@ -2782,27 +3291,27 @@ Median koko: %2 Soitit puhelun - + %1 placed a call %1 soitti puhelun - + You answered a call Vastasit puheluun - + %1 answered a call %1 vastasi puheluun - + You ended a call Lopetit puhelun - + %1 ended a call %1 lopetti puhelun @@ -2810,7 +3319,7 @@ Median koko: %2 utils - + Unknown Message Type Tuntematon viestityyppi diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index c6d42299..f57f8984 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Appel en coursโ€ฆ @@ -56,7 +56,7 @@ CallInvite - + Video Call Appel vidรฉo @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Appel vidรฉo @@ -91,17 +91,17 @@ Accept - Dรฉcrocher + Dรฉcrocher Unknown microphone: %1 - Microphone inconnu  : %1 + Microphone inconnu : %1 Unknown camera: %1 - Camรฉra inconnue  : %1 + Camรฉra inconnue : %1 @@ -111,37 +111,37 @@ No microphone found. - Pas de microphone trouvรฉ. + Aucun microphone trouvรฉ. CallManager - + Entire screen - L'รฉcran complet + Tout l'รฉcran ChatPage - + Failed to invite user: %1 ร‰chec lors de l'invitation de %1 - + Invited user: %1 - %1 a รฉtรฉ invitรฉ(e) + Utilisateur %1 invitรฉ(e) - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. La migration du cache vers la version actuelle a รฉchouรฉ. Cela peut arriver pour diffรฉrentes raisons. Signalez le problรจme et essayez d'utiliser une ancienne version en attendant. Vous pouvez รฉgalement supprimer le cache manuellement. - + Confirm join Confirmez la participation @@ -151,23 +151,23 @@ Voulez-vous vraiment rejoindre %1 ? - + Room %1 created. Salon %1 crรฉรฉ. - + Confirm invite Confirmer l'invitation - + Do you really want to invite %1 (%2)? Voulez-vous vraiment inviter %1 (%2) ? - + Failed to invite %1 to %2: %3 ร‰chec de l'invitation de %1 dans %2 : %3 @@ -182,7 +182,7 @@ Voulez-vous vraiment expulser %1 (%2) ? - + Kicked user: %1 L'utilisateur %1 a รฉtรฉ expulsรฉ. @@ -197,9 +197,9 @@ Voulez-vous vraiment bannir %1 (%2) ? - + Failed to ban %1 in %2: %3 - L'utilisateur %1 n'a pas pu รชtre banni de %2 : %3 + ร‰chec du bannissement de %1 de %2 : %3 @@ -217,7 +217,7 @@ Voulez-vous vraiment annuler le bannissement de %1 (%2) ? - + Failed to unban %1 in %2: %3 ร‰chec de l'annulation du bannissement de %1 dans %2 : %3 @@ -227,12 +227,12 @@ %1 n'est plus banni(e) - + Do you really want to start a private chat with %1? - Voulez-vous vraimer commencer une discussion privรฉe avec %1 ? + Voulez-vous vraiment commencer une discussion privรฉe avec %1 ? - + Cache migration failed! ร‰chec de la migration du cache ! @@ -247,33 +247,35 @@ Le cache sur votre disque est plus rรฉcent que cette version de Nheko ne supporte. Veuillez mettre ร  jour ou supprimer votre cache. - + Failed to restore OLM account. Please login again. ร‰chec de la restauration du compte OLM. Veuillez vous reconnecter. + + Failed to restore save data. Please login again. ร‰chec de la restauration des donnรฉes sauvegardรฉes. Veuillez vous reconnecter. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. ร‰chec de la configuration des clรฉs de chiffrement. Rรฉponse du serveur : %1 %2. Veuillez rรฉessayer plus tard. - - + + Please try to login again: %1 - Veuillez vous reconnecter : %1 + Veuillez re-tenter vous reconnecter : %1 - + Failed to join room: %1 Impossible de rejoindre le salon : %1 - + You joined the room Vous avez rejoint le salon @@ -283,7 +285,7 @@ Impossible de supprimer l'invitation : %1 - + Room creation failed: %1 ร‰chec de la crรฉation du salon : %1 @@ -293,9 +295,9 @@ Impossible de quitter le salon : %1 - + Failed to kick %1 from %2: %3 - ร‰chec de l'expulsion de %1 depuis %2  : %3 + ร‰chec de l'expulsion de %1 de %2  : %3 @@ -303,7 +305,7 @@ Hide rooms with this tag or from this space by default. - + Cacher par dรฉfaut les salons avec cette รฉtiquette ou de cet espace. @@ -311,63 +313,63 @@ All rooms - Tous les salons + Tous les salons Shows all rooms without filtering. - + Montre tous les salons sans filtrer. Favourites - + Favoris Rooms you have favourited. - + Vos salons favoris. Low Priority - Basse prioritรฉ + Prioritรฉ basse Rooms with low priority. - + Salons ร  prioritรฉ basse. Server Notices - Notifications du serveur + Notifications du serveur Messages from your server or administrator. - + Messages de votre serveur ou administrateur. CrossSigningSecrets - + Decrypt secrets Dรฉchiffrer les secrets Enter your recovery key or passphrase to decrypt your secrets: - Entrez votre clรฉ de rรฉcupรฉration ou phrase de passe pour dรฉchiffrer vos secrets  : + Entrez votre clรฉ de rรฉcupรฉration ou phrase de passe pour dรฉchiffrer vos secrets : - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - Entrez votre clรฉ de rรฉcupรฉration ou votre phrase de passe nommรฉe %1 pour dรฉchiffrer vos secrets  : + Entrez votre clรฉ de rรฉcupรฉration ou votre phrase de passe nommรฉe %1 pour dรฉchiffrer vos secrets : - + Decryption failed ร‰chec du dรฉchiffrement @@ -431,7 +433,7 @@ Chercher - + People Personnes @@ -481,17 +483,60 @@ Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification! - Veuillez vรฉrifier les รฉmoji suivantes. Vous devriez voir les mรชmes รฉmoji des deux cรดtรฉs. Si celles-ci diffรจrent, veuillez choisir ยซ Elles sont diffรฉrentes ! ยป pour annuler la vรฉrification ! + Veuillez vรฉrifier les รฉmoji suivants. Vous devriez voir les mรชmes รฉmoji des deux cรดtรฉs. S'ils diffรจrent, veuillez choisir ยซ Ils sont diffรฉrents ! ยป pour annuler la vรฉrification ! They do not match! - Elles sont diffรฉrentes ! + Ils sont diffรฉrents ! They match! - Elles sont identiques ! + Ils sont identiques ! + + + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Il n'y a pas de clรฉ pour dรฉverrouiller ce message. Nous avons demandรฉ la clรฉ automatiquement, mais vous pouvez tenter de la demander ร  nouveau si vous รชtes impatient. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Ce message n'a pas pu รชtre dรฉchiffrรฉ, car nous n'avons une clef que pour des messages plus rรฉcents. Vous pouvez demander l'accรจs ร  ce message. + + + + There was an internal error reading the decryption key from the database. + Une erreur interne s'est produite durant la lecture de la clef de dรฉchiffrement depuis la base de donnรฉes. + + + + There was an error decrypting this message. + Une erreur s'est produite durant le dรฉchiffrement de ce message. + + + + The message couldn't be parsed. + Le message n'a pas pu รชtre traitรฉ. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + La clef de chiffrement a รฉtรฉ rรฉutilisรฉe ! Quelqu'un essaye peut-รชtre d'insรฉrer de faux messages dans ce chat ! + + + + Unknown decryption error + Erreur de dรฉchiffrement inconnue + + + + Request key + Demander la clef @@ -504,62 +549,17 @@ Encrypted by a verified device - + Chiffrรฉ par un appareil vรฉrifiรฉ Encrypted by an unverified device, but you have trusted that user so far. - + Chiffrรฉ par un appareil non vรฉrifiรฉ, mais vous avez dรฉjร  fait confiance ร  ce contact. - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- ร‰vรจnement chiffrรฉ (pas de clรฉ trouvรฉe pour le dรฉchiffrement) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - -- ร‰vรฉnement chiffrรฉ (clรฉ invalide pour cet index) -- - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- ร‰chec du dรฉchiffrement (รฉchec de la rรฉcupรฉration des clรฉs megolm depuis la base de donnรฉes) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Erreur de dรฉchiffrement (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- ร‰vรจnement chiffrรฉ (type d'รฉvรจnement inconnu) -- - - - - -- Replay attack! This message index was reused! -- - -- Attaque par rejeu (replay attack) ! Cet index de message a รฉtรฉ rรฉutilisรฉ ! -- - - - - -- Message by unverified device! -- - -- Message d'un appareil non vรฉrifiรฉ  -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Chiffrรฉ par un appareil non vรฉrifiรฉ, ou la clef provient d'une source non sรปre comme la sauvegarde des clefs. @@ -581,17 +581,26 @@ - Device verification timed out. Dรฉlai dรฉpassรฉ pour la vรฉrification de l'appareil. - + Other party canceled the verification. Le correspondant a annulรฉ la vรฉrification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close Fermer @@ -601,7 +610,82 @@ Forward Message - + Transfรฉrer le message + + + + ImagePackEditorDialog + + + Editing image pack + Modification du paquet d'images + + + + Add images + Ajouter des images + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Autocollants (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + Clef d'รฉtat + + + + Packname + Nom de paquet + + + + Attribution + Attribution + + + + + Use as Emoji + Utiliser en tant qu'รฉmoji + + + + + Use as Sticker + Utiliser en tant qu'autocollant + + + + Shortcode + Raccourci + + + + Body + Corps + + + + Remove from pack + Retirer du paquet + + + + Remove + Retirer + + + + Cancel + Annuler + + + + Save + Sauvegarder @@ -609,43 +693,58 @@ Image pack settings - + Paramรจtres des paquets d'images - + + Create account pack + Crรฉer un paquet de compte + + + + New room pack + Nouveau paquet de salle + + + Private pack - + Paquet privรฉ Pack from this room - + Paquet de cette salle Globally enabled pack - + Paquet activรฉ partout - + Enable globally - + Activer partout Enables this pack to be used in all rooms - + Permet d'utiliser ce paquet dans tous les salons - + + Edit + Modifier + + + Close - Fermer + Fermer InputBar - + Select a file Sรฉlectionnez un fichier @@ -655,7 +754,7 @@ Tous les types de fichiers (*) - + Failed to upload media. Please try again. ร‰chec de l'envoi du mรฉdia. Veuillez rรฉessayer. @@ -663,35 +762,61 @@ InviteDialog - + Invite users to %1 - + Inviter des utilisateurs dans %1 User ID to invite - Identifiant d'utilisateur ร  inviter + Identifiant de l'utilisateur ร  inviter @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @jean:matrix.org Add - + Ajouter Invite - + Inviter Cancel - Annuler + Annuler + + + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + Identifiant ou alias du salon + + + + LeaveRoomDialog + + + Leave room + Quitter le salon + + + + Are you sure you want to leave? + รŠtes-vous sรปrยทe de vouloir quitter ? @@ -704,7 +829,7 @@ e.g @joe:matrix.org - ex : @joe:matrix.org + p. ex : @jean:matrix.org @@ -735,7 +860,7 @@ Si Nheko n'arrive pas ร  trouver votre serveur, il vous proposera de l&apos A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used. - Un nom pour cet appareil, qui sera montrรฉ aux autres utilisateurs lorsque ceux-ci le vรฉrifieront. Si aucun n'est fourni, un nom par dรฉfaut est utilisรฉ. + Un nom pour cet appareil, qui sera montrรฉ aux autres utilisateurs lorsque ceux-ci vรฉrifient vos appareils. Si aucun n'est fourni, un nom par dรฉfaut est utilisรฉ. @@ -745,14 +870,14 @@ Si Nheko n'arrive pas ร  trouver votre serveur, il vous proposera de l&apos server.my:8787 - mon.serveur.fr:8787 + monserveur.example.com:8787 The address that can be used to contact you homeservers client API. Example: https://server.my:8787 L'adresse qui peut รชtre utilisรฉe pour joindre l'API client de votre serveur. -Exemple : https ://monserveur.example.com :8787 +Exemple : https ://monserveur.example.com:8787 @@ -760,27 +885,27 @@ Exemple : https ://monserveur.example.com :8787 CONNEXION - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - Vous avez entrรฉ un identifiant Matrix invalide (exemple correct : @moi :mon.serveur.fr) + Vous avez entrรฉ un identifiant Matrix invalide exemple correct : @moi:monserveur.example.com) - + Autodiscovery failed. Received malformed response. - ร‰chec de la dรฉcouverte automatique. Rรฉponse mal formatรฉe reรงue. + ร‰chec de la dรฉcouverte automatique. Rรฉponse mal formรฉe reรงue. - + Autodiscovery failed. Unknown error when requesting .well-known. ร‰chec de la dรฉcouverte automatique. Erreur inconnue lors de la demande de .well-known. - + The required endpoints were not found. Possibly not a Matrix server. - Les chemins requis n'ont pas รฉtรฉ trouvรฉs. Possible qu'il ne s'agisse pas d'un serveur Matrix. + Les endpoints requis n'ont pas รฉtรฉ trouvรฉs. Ce n'est peut-รชtre pas un serveur Matrix. @@ -788,30 +913,48 @@ Exemple : https ://monserveur.example.com :8787 Rรฉponse mal formรฉe reรงue. Vรฉrifiez que le nom de domaine du serveur est valide. - + An unknown error occured. Make sure the homeserver domain is valid. Une erreur inconnue est survenue. Vรฉrifiez que le nom de domaine du serveur est valide. - + SSO LOGIN CONNEXION SSO - + Empty password Mot de passe vide - + SSO login failed ร‰chec de la connexion SSO + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed retirรฉ @@ -822,7 +965,7 @@ Exemple : https ://monserveur.example.com :8787 Chiffrement activรฉ - + room name changed to: %1 nom du salon changรฉ en : %1 @@ -834,7 +977,7 @@ Exemple : https ://monserveur.example.com :8787 topic changed to: %1 - sujet changรฉ pour : %1 + sujet changรฉ en : %1 @@ -844,7 +987,7 @@ Exemple : https ://monserveur.example.com :8787 %1 changed the room avatar - + %1 a changรฉ l'avatar du salon @@ -881,6 +1024,11 @@ Exemple : https ://monserveur.example.com :8787 Negotiating call... Nรฉgociation de l'appelโ€ฆ + + + Allow them in + Les laisser entrer + MessageInput @@ -905,9 +1053,9 @@ Exemple : https ://monserveur.example.com :8787 ร‰crivez un messageโ€ฆ - + Stickers - + Autocollants @@ -922,13 +1070,13 @@ Exemple : https ://monserveur.example.com :8787 You don't have permission to send messages in this room - + Vous n'avez pas l'autorisation d'envoyer des messages dans ce salon MessageView - + Edit Modifier @@ -948,74 +1096,81 @@ Exemple : https ://monserveur.example.com :8787 Options - + + &Copy - + &Copier - + + Copy &link location - + Copier l'adresse du &lien - + Re&act - + Rรฉ&agir Repl&y - + &Y rรฉpondre &Edit - + &Editer Read receip&ts - + Accusรฉs de lec&ture &Forward - + &Faire suivre &Mark as read - + &Marquer comme lu View raw message - Voir le message brut + Voir le message brut View decrypted raw message - Voir le message dรฉchiffrรฉ brut + Voir le message dรฉchiffrรฉ brut Remo&ve message - + Enle&ver le message &Save as - + Enregistrer &sous &Open in external program - + &Ouvrir dans un programme externe Copy link to eve&nt - + Copier le lien vers l'รฉvรจne&nement + + + + &Go to quoted message + Aller au messa&ge citรฉ @@ -1031,14 +1186,19 @@ Exemple : https ://monserveur.example.com :8787 Demande de vรฉrification reรงue - + + 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.) + + + + 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? - Pour permettre aux autres utilisateurs de vรฉrifier quels appareils de votre compte sont sous votre contrรดle, vous pouvez vรฉrifier ceux-ci. Cela permet รฉgalement ร  ces appareils de sauvegarder vos clรฉs de chiffrement automatiquement. Vรฉrifier %1 maintenant ? + Pour permettre aux autres utilisateurs de vรฉrifier quels appareils de votre compte sont rรฉellement les vรดtres, vous pouvez les vรฉrifier. Cela permet รฉgalement ร  la sauvegarde des clรฉs de fonctionner automatiquement. Vรฉrifier %1 maintenant ? To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party. - Pour vous assurer que personne n'intercepte vos communications chiffrรฉes, vous pouvez vรฉrifier le correspondant. + Pour vous assurer que personne ne puisse intercepter vos communications chiffrรฉes, vous pouvez vรฉrifier le correspondant. @@ -1076,33 +1236,29 @@ Exemple : https ://monserveur.example.com :8787 Accepter + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 a envoyรฉ un message chiffrรฉ - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - * %1 %2 - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message %1 a rรฉpondu : %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1  : %2 - @@ -1112,7 +1268,7 @@ Exemple : https ://monserveur.example.com :8787 %1 replied to a message - %1 a rรฉpondu a un message + %1 a rรฉpondu ร  un message @@ -1133,7 +1289,7 @@ Exemple : https ://monserveur.example.com :8787 Pas de microphone trouvรฉ. - + Voice Vocal @@ -1164,9 +1320,9 @@ Exemple : https ://monserveur.example.com :8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. - Crรฉer un profil unique, vous permettant de vous connecter simultanรฉment ร  plusieurs comptes et ร  lancer plusieurs instances de nheko. + Crรฉer un profil unique, vous permettant de vous connecter simultanรฉment ร  plusieurs comptes et de lancer plusieurs instances de nheko. @@ -1179,21 +1335,37 @@ Exemple : https ://monserveur.example.com :8787 nom du profil + + ReadReceipts + + + Read receipts + Accusรฉs de lecture + + + + ReadReceiptsModel + + + Yesterday, %1 + Hier, %1 + + RegisterPage - + Username Nom d'utilisateur - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - Le nom d'utilisateur ne doit pas รชtre vide, et ne peut contenir que les caractรจres a ร  z, 0 ร  9, et ยซ . _ = - / ยป. + Le nom d'utilisateur ne doit pas รชtre vide, et ne peut contenir que les caractรจres a-z, 0-9, ., _, =, -, et /. - + Password Mot de passe @@ -1213,7 +1385,7 @@ Exemple : https ://monserveur.example.com :8787 Serveur - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. Un serveur qui autorise les crรฉations de compte. Matrix รฉtant dรฉcentralisรฉ, vous devez tout d'abord trouver un serveur sur lequel vous pouvez vous inscrire, ou bien hรฉberger le vรดtre. @@ -1223,52 +1395,42 @@ Exemple : https ://monserveur.example.com :8787 S'ENREGISTRER - - No supported registration flows! - Pas de mรฉthode d'inscription supportรฉe ! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - Un ou plusieurs champs ont des entrรฉes invalides. Veuillez les corriger et rรฉessayer. - - - + Autodiscovery failed. Received malformed response. - ร‰chec de la dรฉcouverte automatique. Rรฉponse mal formatรฉe reรงue. + ร‰chec de la dรฉcouverte automatique. Rรฉponse mal formรฉe reรงue. - + Autodiscovery failed. Unknown error when requesting .well-known. - ร‰chec de la dรฉcouverte automatique. Erreur inconnue lors de la demande de .well-known. + ร‰chec de la dรฉcouverte automatique. Erreur inconnue lors de la demande de .well-known. - + The required endpoints were not found. Possibly not a Matrix server. - Les chemins requis n'ont pas รฉtรฉ trouvรฉs. Possible qu'il ne s'agisse pas d'un serveur Matrix. + Les endpoints requis n'ont pas รฉtรฉ trouvรฉs. Ce n'est peut-รชtre pas un serveur Matrix. Received malformed response. Make sure the homeserver domain is valid. - Rรฉponse mal formรฉe reรงue. Vรฉrifiez que le nom de domaine du serveur est valide. + Rรฉponse mal formรฉe reรงue. Vรฉrifiez que le nom de domaine du serveur est valide. An unknown error occured. Make sure the homeserver domain is valid. - Une erreur inconnue est survenue. Vรฉrifiez que le nom de domaine du serveur est valide. + Une erreur inconnue est survenue. Vรฉrifiez que le nom de domaine du serveur est valide. - + Password is not long enough (min 8 chars) Le mot de passe n'est pas assez long (8 caractรจres minimum) - + Passwords don't match Les mots de passe ne sont pas identiques - + Invalid server name Le nom du serveur est invalide @@ -1276,7 +1438,7 @@ Exemple : https ://monserveur.example.com :8787 ReplyPopup - + Close Fermer @@ -1286,10 +1448,28 @@ Exemple : https ://monserveur.example.com :8787 Abandonner la modification + + RoomDirectory + + + Explore Public Rooms + Explorer les salons publics + + + + Search for public rooms + Rechercher des salons publics + + + + Choose custom homeserver + + + RoomInfo - + no version stored pas de version enregistrรฉe @@ -1297,137 +1477,170 @@ Exemple : https ://monserveur.example.com :8787 RoomList - + New tag - + Nouvelle รฉtiquette Enter the tag you want to use: - - - - - Leave Room - - - - - Are you sure you want to leave this room? - + Entrez l'รฉtiquette que vous voulez utiliser : Leave room - Quitter le salon + Quitter le salon Tag room as: - ร‰tiqueter le salon comme : + ร‰tiqueter le salon comme : Favourite - Favori + Favori Low priority - + Prioritรฉ basse Server notice - + Notification du serveur Create new tag... - + Crรฉer une nouvelle รฉtiquetteโ€ฆ - + Status Message - + Message de statut Enter your status message: - + Entrez votre message de statut : Profile settings - + Paramรจtres de profil Set status message + Changer le message de statut + + + + Logout + Dรฉconnexion + + + + Encryption not set up + Cross-signing setup has not run yet. - - Logout - Se dรฉconnecter + + Unverified login + The user just signed in with this device and hasn't verified their master key. + - + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Fermer + + + Start a new chat - Commencer une discussion + Commencer une nouvelle discussion Join a room - Rejoindre un salon + Rejoindre un salon Create a new room - + Crรฉer un nouveau salon Room directory - Annuaire des salons + Annuaire des salons - + User settings - Paramรจtres utilisateur + Paramรจtres utilisateur RoomMembers - + Members of %1 - + Membres de %1 - + %n people in %1 Summary above list of members - - - + + %n personne dans %1 + %n personnes dans %1 Invite more people - + Inviter plus de personnes + + + + This room is not encrypted! + Ce salon n'est pas chiffrรฉ ! + + + + This user is verified. + Cet utilisateur est vรฉrifiรฉ. + + + + This user isn't verified, but is still using the same master key from the first time you met. + Cet utilisateur n'est pas vรฉrifiรฉ, mais utilise toujours la mรชme clef maรฎtresse que la premiรจre fois que vous vous รชtes rencontrรฉs. + + + + This user has unverified devices! + Cet utilisateur a des appareils non vรฉrifiรฉs ! RoomSettings - + Room Settings Configuration du salon - + %1 member(s) %1 membre(s) @@ -1457,7 +1670,12 @@ Exemple : https ://monserveur.example.com :8787 Tous les messages - + + Room access + Accรจs au salon + + + Anyone and guests Tous le monde et les invitรฉs @@ -1472,7 +1690,17 @@ Exemple : https ://monserveur.example.com :8787 Utilisateurs invitรฉs - + + By knocking + En toquant + + + + Restricted by membership in other rooms + Restreint par l'appartenance ร  d'autre salons + + + Encryption Chiffrement @@ -1485,22 +1713,22 @@ Exemple : https ://monserveur.example.com :8787 Encryption is currently experimental and things might break unexpectedly. <br> Please take note that it can't be disabled afterwards. - Le chiffrement actuellement expรฉrimental et des comportements inattendus peuvent รชtre rencontrรฉs. <br>Veuillez noter qu'il n'est pas possible de le dรฉsactiver par la suite. + Le chiffrement est actuellement expรฉrimental et des comportements inattendus peuvent apparaรฎtre.<br>Veuillez noter qu'il n'est pas possible de le dรฉsactiver par la suite. Sticker & Emote Settings - + Paramรจtres des autocollants & emotes Change - + Modifier Change what packs are enabled, remove packs or create new ones - + Modifier quels paquets sont activรฉs, retirer des paquets ou bien en crรฉer de nouveaux @@ -1518,12 +1746,12 @@ Exemple : https ://monserveur.example.com :8787 Version du salon - + Failed to enable encryption: %1 - ร‰chec de l'activation du chiffrement  : %1 + ร‰chec de l'activation du chiffrement : %1 - + Select an avatar Sรฉlectionner un avatar @@ -1535,35 +1763,63 @@ Exemple : https ://monserveur.example.com :8787 The selected file is not an image - Le fichier sรฉlectionnรฉ n'est pas une image + Le fichier sรฉlectionnรฉ n'est pas une image Error while reading file: %1 - Erreur lors de la lecture du fichier  : %1 + Erreur lors de la lecture du fichier : %1 - - + + Failed to upload image: %s - ร‰chec de l'envoi de l'image  : %s + ร‰chec de l'envoi de l'image : %s RoomlistModel - + Pending invite. - + Invitation en attente. - + Previewing this room + Prรฉvisualisation du salon + + + + No preview available + Aucune prรฉvisualisation disponible + + + + Root + + + Please enter your login password to continue: - - No preview available + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. @@ -1577,12 +1833,12 @@ Exemple : https ://monserveur.example.com :8787 Window: - Fenรชtre  : + Fenรชtre : Frame rate: - Frรฉquence d'images  : + Frรฉquence d'images : @@ -1618,7 +1874,122 @@ Exemple : https ://monserveur.example.com :8787 Cancel - Annuler + Annuler + + + + SecretStorage + + + Failed to connect to secret storage + ร‰chec de la connexion au stockage des secrets + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + ร‰chec de la mise ร  jour du paquet d'images : %1 + + + + Failed to delete old image pack: %1 + ร‰chec de l'effacement de l'ancien paquet d'images : %1 + + + + Failed to open image: %1 + ร‰chec de l'ouverture de l'image : %1 + + + + Failed to upload image: %1 + ร‰chec de l'envoi de l'image : %1 @@ -1649,7 +2020,7 @@ Exemple : https ://monserveur.example.com :8787 Search - Chercher + Rechercher @@ -1662,7 +2033,7 @@ Exemple : https ://monserveur.example.com :8787 Verification successful! Both sides verified their devices! - Vรฉrification rรฉussie ! Les deux cรดtรฉs ont vรฉrifiรฉ leur appareil ! + Vรฉrification rรฉussie ! Les deux cรดtรฉs ont vรฉrifiรฉ leur appareil ! @@ -1673,18 +2044,18 @@ Exemple : https ://monserveur.example.com :8787 TimelineModel - + Message redaction failed: %1 ร‰chec de la suppression du message : %1 - + Failed to encrypt event, sending aborted! ร‰chec du chiffrement de l'รฉvรจnement, envoi abandonnรฉ ! - + Save image Enregistrer l'image @@ -1704,7 +2075,7 @@ Exemple : https ://monserveur.example.com :8787 Enregistrer le fichier - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1713,9 +2084,9 @@ Exemple : https ://monserveur.example.com :8787 - + %1 opened the room to the public. - %1 a rendu le salon ouvert au public. + %1 a ouvert le salon au public. @@ -1723,7 +2094,17 @@ Exemple : https ://monserveur.example.com :8787 %1 a rendu le rendu le salon joignable uniquement sur invitation. - + + %1 allowed to join this room by knocking. + %1 a permis de rejoindre ce salon en toquant. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 a permis aux membres des salons suivants de rejoindre automatiquement ce salon : %2 + + + %1 made the room open to guests. %1 a rendu le salon ouvert aux invitรฉs. @@ -1743,14 +2124,14 @@ Exemple : https ://monserveur.example.com :8787 %1 a rendu l'historique du salon visible aux membre ร  partir de cet instant. - + %1 set the room history visible to members since they were invited. %1 a rendu l'historique visible aux membres ร  partir de leur invitation. - + %1 set the room history visible to members since they joined the room. - %1 a rendu l'historique du salon visible ร  partir de l'instant oรน un membre le rejoint. + %1 a rendu l'historique du salon visible aux membres ร  partir de l'instant oรน ils le rejoignent. @@ -1758,27 +2139,32 @@ Exemple : https ://monserveur.example.com :8787 %1 a changรฉ les permissions du salon. - + %1 was invited. %1 a รฉtรฉ invitรฉ(e). - + %1 changed their avatar. %1 a changรฉ son avatar. %1 changed some profile info. - %1 a changรฉ ses informations de profil. + %1 a changรฉ des informations de profil. - + %1 joined. %1 a rejoint le salon. - + + %1 joined via authorisation from %2's server. + %1 a rejoint via une autorisation de la part du serveur de %2. + + + %1 rejected their invite. %1 a rejetรฉ son invitation. @@ -1808,34 +2194,34 @@ Exemple : https ://monserveur.example.com :8787 %1 a รฉtรฉ banni. - + Reason: %1 - + Raison : %1 - + %1 redacted their knock. - %1 ne frappe plus au salon. + %1 a arrรชtรฉ de toquer. - + You joined this room. Vous avez rejoint ce salon. - + %1 has changed their avatar and changed their display name to %2. - + %1 a changรฉ son avatar et changรฉ son surnom en %2. - + %1 has changed their display name to %2. - + %1 a changรฉ son surnom en %2. - + Rejected the knock from %1. - %1 a รฉtรฉ rejetรฉ aprรจs avoir frappรฉ au salon. + %1 a รฉtรฉ rejetรฉ aprรจs avoir toquรฉ. @@ -1846,13 +2232,13 @@ Exemple : https ://monserveur.example.com :8787 %1 knocked. - %1 a frappรฉ au salon. + %1 a toquรฉ. TimelineRow - + Edited Modifiรฉ @@ -1860,58 +2246,75 @@ Exemple : https ://monserveur.example.com :8787 TimelineView - + No room open Aucun salon ouvert - + + No preview available + Aucune prรฉvisualisation disponible + + + %1 member(s) - %1 membre(s) + %1 membre(s) join the conversation - + rejoindre la conversation accept invite - + accepter l'invitation decline invite - + dรฉcliner l'invitation Back to room list - Revenir ร  la liste des salons - - - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - Pas de discussion privรฉe et chiffrรฉe trouvรฉe avec cet utilisateur. Crรฉez-en une et rรฉessayez. + Revenir ร  la liste des salons TopBar - + Back to room list Revenir ร  la liste des salons - + No room selected Pas de salon sรฉlectionnรฉ - + + This room is not encrypted! + Ce salon n'est pas chiffrรฉ ! + + + + This room contains only verified devices. + Ce salon ne contient que des appareils vรฉrifiรฉs. + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + Ce salon contient des appareils non vรฉrifiรฉs ! + + + Room options Options du salon @@ -1949,10 +2352,35 @@ Exemple : https ://monserveur.example.com :8787 Quitter + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Veuillez entrer un jeton d'enregistrement valide. + + + + Invalid token + + + UserProfile - + Global User Profile Profil gรฉnรฉral de l'utilisateur @@ -1962,35 +2390,100 @@ Exemple : https ://monserveur.example.com :8787 Profil utilisateur spรฉcifique au salon - - + + Change avatar globally. + Changer l'image de profil partout. + + + + Change avatar. Will only apply to this room. + Changer l'image de profil. Ne s'appliquera qu'ร  ce salon. + + + + Change display name globally. + Changer de surnom partout. + + + + Change display name. Will only apply to this room. + Changer de surnom. Ne s'appliquera qu'ร  ce salon. + + + + Room: %1 + Salon : %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + Ceci est un profil spรฉcifique ร  un salon. Le surnom et l'image de profil peuvent รชtre diffรฉrents de leurs versions globales. + + + + Open the global profile for this user. + Ouvrir le profil global de cet utilisateur. + + + + Verify Vรฉrifier - - Ban the user - Bannir l'utilisateur - - - - Start a private chat - Crรฉer une nouvelle discussion privรฉe + + Start a private chat. + Dรฉmarrer une discussion privรฉe. - Kick the user - Expulser l'utilisateur + Kick the user. + Expulser l'utilisateur. - + + Ban the user. + Bannir l'utilisateur. + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify Dรฉ-vรฉrifier - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar - Sรฉlectionner un avatar + Sรฉlectionnez un avatar @@ -2011,8 +2504,8 @@ Exemple : https ://monserveur.example.com :8787 UserSettings - - + + Default Dรฉfaut @@ -2020,9 +2513,9 @@ Exemple : https ://monserveur.example.com :8787 UserSettingsPage - + Minimize to tray - Rรฉduire ร  la barre des tรขches + Rรฉduire dans la barre des tรขches @@ -2030,22 +2523,22 @@ Exemple : https ://monserveur.example.com :8787 Dรฉmarrer dans la barre des tรขches - + Group's sidebar Barre latรฉrale des groupes - + Circular Avatars Avatars circulaires - + profile: %1 profil : %1 - + Default Dรฉfaut @@ -2057,7 +2550,7 @@ Exemple : https ://monserveur.example.com :8787 Cross Signing Keys - Clรฉs d'auto-vรฉrification (Cross-Signing) + Clรฉs de signature croisรฉe (Cross-Signing) @@ -2070,14 +2563,14 @@ Exemple : https ://monserveur.example.com :8787 Tร‰Lร‰CHARGER - + Keep the application running in the background after closing the client window. - Conserver l'application en arriรจre plan aprรจs la fermeture de la fenรชtre du client. + Conserver l'application en arriรจre-plan aprรจs avoir fermรฉ la fenรชtre du client. Start the application in the background without showing the client window. - Dรฉmarrer l'application en arriรจre plan sans montrer la fenรชtre du client. + Dรฉmarrer l'application en arriรจre-plan sans montrer la fenรชtre du client. @@ -2086,10 +2579,20 @@ OFF - square, ON - Circle. Change l'apparence des avatars des utilisateurs dans les discussions. OFF โ€“ carrรฉ, ON โ€“ cercle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. - Affiche une colonne contenant les groupes et tags ร  cรดtรฉ de la liste des salons. + Affiche une colonne contenant les groupes et รฉtiquettes ร  cรดtรฉ de la liste des salons. @@ -2116,7 +2619,7 @@ be blurred. sera floutรฉe. - + Privacy screen timeout (in seconds [0 - 3600]) Attente pour l'activation de la protection anti-indiscrรฉtion (en secondes, 0 ร  3600) @@ -2128,7 +2631,7 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds Temps d'attente (en secondes) avant le floutage des conversations lorsque la fenรชtre n'est plus active. Rรฉgler ร  0 pour flouter immรฉdiatement lorsque la fenรชtre n'est plus au premier plan. -Valeur maximale de une heure (3600 secondes). +Valeur maximale d'une heure (3600 secondes). @@ -2138,12 +2641,12 @@ Valeur maximale de une heure (3600 secondes). Show buttons to quickly reply, react or access additional options next to each message. - Montre les boutons de rรฉponse, rรฉaction ou options additionnelles prรจs de chaque message. + Montre les boutons de rรฉponse, rรฉaction et options additionnelles prรจs de chaque message. Limit width of timeline - Limiter la largeur de l'historique + Limiter la largeur de la discussion @@ -2173,12 +2676,12 @@ Ceci activera ou dรฉsactivera รฉgalement l'envoi de notifications similaire If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room. If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. Montre les salons qui contiennent de nouveaux messages en premier. -Si non activรฉ, la liste des salons sera uniquement triรฉ en fonction de la date du dernier message. +Si non activรฉ, la liste des salons sera uniquement triรฉe en fonction de la date du dernier message. Si activรฉ, les salons qui ont des notifications actives (le petit cercle avec un chiffre dedans) seront affichรฉs en premier. -Les salons que vous avez rendu silencieux seront toujours triรฉs par date du dernier message, car ceux-ci sont considรฉrรฉs comme moins importants. +Les salons que vous avez rendu silencieux seront toujours triรฉs par date du dernier message, puisqu'ils sont considรฉrรฉs comme moins importants. - + Read receipts Accusรฉs de lecture @@ -2190,7 +2693,7 @@ Status is displayed next to timestamps. Le statut est montrรฉ prรจs de la date des messages. - + Send messages as Markdown Composer les messages au format Markdown @@ -2203,13 +2706,18 @@ Lorsque dรฉsactivรฉ, tous les messages sont envoyรฉs en texte brut. + Play animated images only on hover + Ne jouer les images animรฉes que quand survolรฉes + + + Desktop notifications - Notifier sur le bureau + Notifications sur le bureau Notify about received message when the client is not currently focused. - Notifie des messages reรงus lorsque la fenรชtre du client n'est pas focalisรฉe. + Notifie des messages reรงus lorsque la fenรชtre du client n'est pas active. @@ -2236,20 +2744,55 @@ Cela met l'application en รฉvidence dans la barre des tรขches. Large Emoji in timeline - Grandes รฉmoticรดnes dans la discussion + Grands emojis dans la discussion Make font size larger if messages with only a few emojis are displayed. - Augmente la taille de la police lors de l'affichage de messages contenant uniquement quelques emojis. + Augmente la taille de la police des messages contenant uniquement quelques emojis. - + + Send encrypted messages to verified users only + N'envoyer des messages chiffrรฉs qu'aux utilisateurs vรฉrifiรฉs + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Requiert qu'un utilisateur soit vรฉrifiรฉ pour lui envoyer des messages chiffrรฉs. La sรฉcuritรฉ en est amรฉliorรฉe, mais le chiffrement de bout en bout devient plus fastidieux. + + + Share keys with verified users and devices Partager vos clรฉs avec les utilisateurs et appareils que vous avez vรฉrifiรฉs - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Rรฉpond automatiquement aux requรชtes de clefs des autres utilisateurs, s'ils sont vรฉrifiรฉs, mรชme si cet appareil ne devrait pas avoir accรจs ร  ces clefs autrement. + + + + Online Key Backup + Sauvegarde des clefs en ligne + + + + Download message encryption keys from and upload to the encrypted online key backup. + Tรฉlรฉcharge les clefs de chiffrement de message depuis et envoie vers la sauvegarde chiffrรฉe en ligne de clefs. + + + + Enable online key backup + Activer la sauvegarde de clefs en ligne + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + Les auteurs de Nheko ne recommandent pas d'activer la sauvegarde en ligne de clefs jusqu'ร  ce que la sauvegarde symรฉtrique en ligne de clefs soit disponible. Activer quand mรชme ? + + + CACHED EN CACHE @@ -2259,14 +2802,14 @@ Cela met l'application en รฉvidence dans la barre des tรขches.PAS DANS LE CACHE - + Scale factor Facteur d'รฉchelle Change the scale factor of the whole user interface. - Agrandit l'interface entiรจre de ce facteur. + Agrandit l'interface entiรจre par ce facteur. @@ -2334,7 +2877,7 @@ Cela met l'application en รฉvidence dans la barre des tรขches.Empreinte de l'appareil - + Session Keys Clรฉs de session @@ -2354,39 +2897,39 @@ Cela met l'application en รฉvidence dans la barre des tรขches.CHIFFREMENT - + GENERAL Gร‰Nร‰RAL - + INTERFACE INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Joue les images comme les GIFs ou WEBPs uniquement quand la souris est au-dessus. + + + Touchscreen mode Mode รฉcran tactile Will prevent text selection in the timeline to make touch scrolling easier. - Empรชchera la sรฉlection de texte dans la discussion pour faciliter le dรฉfilement tactile. + Empรชche la sรฉlection de texte dans la discussion pour faciliter le dรฉfilement tactile. Emoji Font Family - Nom de Police Emoji + Nom de police pour รฉmoji - - Automatically replies to key requests from other users, if they are verified. - Automatiquement rรฉpondre aux demandes de clรฉs de dรฉchiffrement des autres utilisateurs, si ceux-ci sont vรฉrifiรฉs. - - - + Master signing key - Clรฉ de signature de l'utilisateur + Clรฉ de signature maรฎtresse de l'utilisateur @@ -2401,12 +2944,12 @@ Cela met l'application en รฉvidence dans la barre des tรขches. The key to verify other users. If it is cached, verifying a user will verify all their devices. - La clรฉ utilisรฉe pour vรฉrifier d'autres utilisateurs. Si celle-ci est cachรฉe, vรฉrifier un utilisateur vรฉrifiera tous ses appareils. + La clรฉ utilisรฉe pour vรฉrifier d'autres utilisateurs. Si celle-ci est dans le cache, vรฉrifier un utilisateur vรฉrifiera tous ses appareils. - + Self signing key - Clรฉ d'auto-vรฉrification + Clรฉ d'auto-vรฉrification @@ -2416,12 +2959,12 @@ Cela met l'application en รฉvidence dans la barre des tรขches. Backup key - Clรฉ de secours + Clรฉ de secours The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server. - La clรฉ utilisรฉe pour dรฉchiffrer les sauvegardes de clรฉ stockรฉes en ligne. Si celle-ci est cachรฉe, vous pouvez activer la sauvegarde de vos clรฉs en ligne afin d'en conserver une copie chiffrรฉe en toute sรฉcuritรฉ sur le serveur. + La clรฉ utilisรฉe pour dรฉchiffrer les sauvegardes de clรฉ stockรฉes en ligne. Si celle-ci est dans le cache, vous pouvez activer la sauvegarde de vos clรฉs en ligne afin d'en conserver une copie chiffrรฉe en toute sรฉcuritรฉ sur le serveur. @@ -2434,14 +2977,14 @@ Cela met l'application en รฉvidence dans la barre des tรขches.Tous les types de fichiers (*) - + Open Sessions File - Ouvrir fichier de sessions + Ouvrir le fichier de sessions - + @@ -2449,26 +2992,26 @@ Cela met l'application en รฉvidence dans la barre des tรขches.Erreur - - + + File Password Mot de passe du fichier - + Enter the passphrase to decrypt the file: - Entrez la clรฉ secrรจte pour dรฉchiffrer le fichier  : + Entrez la phrase de passe pour dรฉchiffrer le fichier : - + The password cannot be empty Le mot de passe ne peut รชtre vide Enter passphrase to encrypt your session keys: - Entrez une clรฉ secrรจte pour chiffrer vos clรฉs de session  : + Entrez une phrase de passe pour chiffrer vos clรฉs de session : @@ -2476,6 +3019,14 @@ Cela met l'application en รฉvidence dans la barre des tรขches.Fichier oรน sauvegarder les clรฉs de session exportรฉes + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Aucune discussion privรฉe chiffrรฉe trouvรฉe avec cet utilisateur. Crรฉez-en une et rรฉessayez. + + Waiting @@ -2583,7 +3134,7 @@ Cela met l'application en รฉvidence dans la barre des tรขches. Open Fallback in Browser - Ouvrir la solution de repli dans le navigateur + Ouvrir la solution de secours dans le navigateur @@ -2598,38 +3149,7 @@ Cela met l'application en รฉvidence dans la barre des tรขches. Open the fallback, follow the steps and confirm after completing them. - Ouvrez la solution de repli, suivez les รฉtapes et confirmez aprรจs les avoir terminรฉes. - - - - dialogs::JoinRoom - - - Join - Rejoindre - - - - Cancel - Annuler - - - - Room ID or alias - Identifiant ou alias du salon - - - - dialogs::LeaveRoom - - - Cancel - Annuler - - - - Are you sure you want to leave? - รŠtes-vous sรปrยทe de vouloir quitter ? + Ouvrez la solution de secours, suivez les รฉtapes et confirmez aprรจs les avoir terminรฉes. @@ -2685,32 +3205,6 @@ Taille du mรฉdia : %2 Rรฉsolvez le reCAPTCHA puis appuyez sur le bouton de confirmation - - dialogs::ReadReceipts - - - Read receipts - Accusรฉs de lecture - - - - Close - Fermer - - - - dialogs::ReceiptItem - - - Today %1 - Aujourd'hui %1 - - - - Yesterday %1 - Hier %1 - - message-description sent: @@ -2724,47 +3218,47 @@ Taille du mรฉdia : %2 %1 a envoyรฉ un message audio - + You sent an image Vous avez envoyรฉ une image - + %1 sent an image %1 a envoyรฉ une image - + You sent a file Vous avez envoyรฉ un fichier - + %1 sent a file %1 a envoyรฉ un fichier - + You sent a video Vous avez envoyรฉ une vidรฉo - + %1 sent a video %1 a envoyรฉ une vidรฉo - + You sent a sticker Vous avez envoyรฉ un autocollant - + %1 sent a sticker %1 a envoyรฉ un autocollant - + You sent a notification Vous avez envoyรฉ une notification @@ -2776,12 +3270,12 @@ Taille du mรฉdia : %2 You: %1 - Vous  : %1 + Vous : %1 - + %1: %2 - %1  : %2 + %1 : %2 @@ -2799,27 +3293,27 @@ Taille du mรฉdia : %2 Vous avez appelรฉ - + %1 placed a call %1 a appelรฉ - + You answered a call Vous avez rรฉpondu ร  un appel - + %1 answered a call %1 a rรฉpondu ร  un appel - + You ended a call Vous avez terminรฉ un appel - + %1 ended a call %1 a terminรฉ un appel @@ -2827,7 +3321,7 @@ Taille du mรฉdia : %2 utils - + Unknown Message Type Type du message inconnu diff --git a/resources/langs/nheko_hu.ts b/resources/langs/nheko_hu.ts index 7c29338c..ffef514b 100644 --- a/resources/langs/nheko_hu.ts +++ b/resources/langs/nheko_hu.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Hรญvรกs... @@ -56,7 +56,7 @@ CallInvite - + Video Call Videรณhรญvรกs @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Videรณhรญvรกs @@ -117,7 +117,7 @@ CallManager - + Entire screen Az egรฉsz kรฉpernyล‘ @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 Nem sikerรผlt meghรญvni a felhasznรกlรณt: %1 - + Invited user: %1 A felhasznรกlรณ meg lett hรญvva: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. A gyorsรญtรณtรกr รกtvitele a jelenlegi verziรณhoz nem sikerรผlt. Ennek tรถbb oka is lehet. Kรฉrlek, รญrj egy hibajelentรฉst รฉs egyelล‘re prรณbรกlj meg egy rรฉgebbi verziรณt hasznรกlni! Alternatรญv megoldรกskรฉnt megprobรกlhatod eltรกvolรญtani a gyorsรญtรณtรกrat kรฉzzel. - + Confirm join Csatlakozรกs megerล‘sรญtรฉse @@ -151,23 +151,23 @@ Biztosan csatlakozni akarsz a(z) %1 szobรกhoz? - + Room %1 created. A %1 nevลฑ szoba lรฉtre lett hozva. - + Confirm invite Meghรญvรกs megerล‘sรญtรฉse - + Do you really want to invite %1 (%2)? Biztos, hogy meg akarod hรญvni a kรถvetkezล‘ felhasznรกlรณt: %1 (%2)? - + Failed to invite %1 to %2: %3 Nem sikerรผlt %1 meghรญvรกsa a(z) %2 szobรกba: %3 @@ -182,7 +182,7 @@ Biztosan ki akarod rรบgni %1 (%2) felhasznรกlรณt? - + Kicked user: %1 Kirรบgott felhasznรกlรณ: %1 @@ -197,7 +197,7 @@ Biztosan ki akarod tiltani %1 (%2) felhasznรกlรณt? - + Failed to ban %1 in %2: %3 Nem sikerรผlt kitiltani %1 felhasznรกlรณt a %2 szobรกbรณl: %3 @@ -217,7 +217,7 @@ Biztosan fel akarod oldani %1 (%2) felhasznรกlรณ kitiltรกsรกt? - + Failed to unban %1 in %2: %3 Nem sikerรผlt feloldani %1 felhasznรกlรณ kitiltรกsรกt a %2 szobรกbรณl: %3 @@ -227,12 +227,12 @@ Kitiltรกs feloldva a felhasznรกlรณnak: %1 - + Do you really want to start a private chat with %1? Biztosan privรกt csevegรฉst akarsz indรญtani %1 felhasznรกlรณval? - + Cache migration failed! Gyorsรญtรณtรกr migrรกciรณ nem sikerรผlt! @@ -247,33 +247,35 @@ A lemezeden lรฉvล‘ gyorsรญtรณtรกr รบjabb, mint amit a Nheko jelenlegi verziรณja tรกmogat. Kรฉrlek, frissรญtsd vagy tรถrรถld a gyorsรญtรณtรกrat! - + Failed to restore OLM account. Please login again. Nem sikerรผlt visszaรกllรญtani az OLM fiรณkot. Kรฉrlek, jelentkezz be ismรฉt! + + Failed to restore save data. Please login again. Nem sikerรผlt visszaรกllรญtani a mentรฉsi adatot. Kรฉrlek, jelentkezz be ismรฉt! - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Nem sikerรผlt beรกllรญtani a titkosรญtรกsi kulcsokat. Vรกlasz a szervertล‘l: %1 %2. Kรฉrlek, prรณbรกld รบjra kรฉsล‘bb! - - + + Please try to login again: %1 Kรฉrlek, prรณbรกlj meg bejelentkezni รบjra: %1 - + Failed to join room: %1 Nem sikerรผlt csatlakozni a szobรกhoz: %1 - + You joined the room Csatlakoztรกl a szobรกhoz @@ -283,7 +285,7 @@ Nem sikerรผlt eltรกvolรญtani a meghรญvรณt: %1 - + Room creation failed: %1 Nem sikerรผlt lรฉtrehozni a szobรกt: %1 @@ -293,7 +295,7 @@ Nem sikerรผlt elhagyni a szobรกt: %1 - + Failed to kick %1 from %2: %3 Nem sikerรผlt kirรบgni %1 felhasznรกlรณt %2 szobรกbรณl: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets Titkos tรกrolรณ feloldรกsa @@ -362,12 +364,12 @@ Add meg a helyreรกllรญtรกsi kulcsodat vagy a jelmondatodat a titkos tรกrolรณ feloldรกsรกhoz: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: Add meg a %1 nevลฑ helyreรกllรญtรกsi kulcsodat vagy a jelmondatodat a titkos tรกrolรณ feloldรกsรกhoz: - + Decryption failed Titkosรญtรกs feloldรกsa nem sikerรผlt @@ -431,7 +433,7 @@ Keresรฉs - + People Emberek @@ -494,6 +496,49 @@ Megegyeznek! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,55 +558,10 @@ - Encrypted by an unverified device + Encrypted by an unverified device or the key is from an untrusted source like the key backup. - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Titkosรญtott esemรฉny (Nem talรกlhatรณk kulcsok a titkosรญtรกs feloldรกsรกhoz) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - -- Titkosรญtott esemรฉny (a kulcs nem รฉrvรฉnyes ehhez az indexhez) -- - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- Hiba a titkosรญtรกs feloldรกsakor (nem sikerรผlt lekรฉrni a megolm kulcsokat az adatbรกzisbรณl) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Hiba a titkosรญtรกs feloldรกsakor (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Titkosรญtott esemรฉny (Ismeretlen esemรฉnytรญpus) -- - - - - -- Replay attack! This message index was reused! -- - -- รšjrajรกtszรกsi tรกmadรกs! Ez az รผzenetindex รบjra fel lett hasznรกlva! -- - - - - -- Message by unverified device! -- - -- Nem hitelesรญtett eszkรถzrล‘l รฉrkezett รผzenet! -- - - Failed @@ -581,17 +581,26 @@ - Device verification timed out. Idล‘tรบllรฉpรฉs az eszkรถzhitelesรญtรฉs alatt. - + Other party canceled the verification. A mรกsik fรฉl megszakรญtotta a hitelesรญtรฉst. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close Bezรกrรกs @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + Mรฉgse + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + Szerkesztรฉs + + + Close Bezรกrรกs @@ -645,7 +744,7 @@ InputBar - + Select a file Fรกjl kivรกlasztรกsa @@ -655,7 +754,7 @@ Minden fรกjl (*) - + Failed to upload media. Please try again. Nem sikerรผlt feltรถlteni a mรฉdiafรกjlt. Kรฉrlek, prรณbรกld รบjra! @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ Mรฉgse + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + Szoba azonosรญtรณja vagy รกlneve + + + + LeaveRoomDialog + + + Leave room + Szoba elhagyรกsa + + + + Are you sure you want to leave? + Biztosan tรกvozni akarsz? + + LoginPage @@ -760,25 +885,25 @@ Pรฉlda: https://szerver.em:8787 BEJELENTKEZร‰S - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org ร‰rvรฉnytelen Matrixazonosรญtรณt adtรกl meg. Pรฉlda: @janos:matrix.org - + Autodiscovery failed. Received malformed response. Az automatikus felderรญtรฉs nem sikerรผlt. Helytelen vรกlasz รฉrkezett. - + Autodiscovery failed. Unknown error when requesting .well-known. Az automatikus felderรญtรฉs nem sikerรผlt. Ismeretlen hiba a .well-known lekรฉrรฉse kรถzben. - + The required endpoints were not found. Possibly not a Matrix server. Nem talรกlhatรณk szรผksรฉges vรฉgpontok. Lehet, hogy nem egy Matrixszerver. @@ -788,35 +913,53 @@ Pรฉlda: https://szerver.em:8787 Helytelen vรกlasz รฉrkezett. Ellenล‘rizd, hogy a homeszervered domainje helyes. - + An unknown error occured. Make sure the homeserver domain is valid. Egy ismeretlen hiba tรถrtรฉnt. Ellenล‘rizd, hogy a homeszervered domainje helyes. - + SSO LOGIN SSO BEJELENTKEZร‰S - + Empty password รœres jelszรณ - + SSO login failed SSO bejelentkezรฉs nem sikerรผlt + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + Encryption enabled Titkosรญtรกs bekapcsolva - + room name changed to: %1 a szoba neve megvรกltoztatva erre: %1 @@ -866,18 +1009,23 @@ Pรฉlda: https://szerver.em:8787 Hรญvรกs elล‘kรฉszรญtรฉseโ€ฆ - + + Allow them in + + + + %1 answered the call. %1 fogadta a hรญvรกst. - + removed eltรกvolรญtva - + %1 ended the call. %1 befejezte a hรญvรกst. @@ -905,7 +1053,7 @@ Pรฉlda: https://szerver.em:8787 รrj egy รผzenetetโ€ฆ - + Stickers @@ -928,7 +1076,7 @@ Pรฉlda: https://szerver.em:8787 MessageView - + Edit Szerkesztรฉs @@ -948,17 +1096,19 @@ Pรฉlda: https://szerver.em:8787 Mลฑveletek - + + &Copy - + + Copy &link location - + Re&act @@ -1017,6 +1167,11 @@ Pรฉlda: https://szerver.em:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1031,7 +1186,12 @@ Pรฉlda: https://szerver.em:8787 Hitelesรญtรฉsi kรฉrรฉs รฉrkezett - + + 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.) + + + + 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? Hogy mรกsok lรกthassรกk, melyik eszkรถz tartozik valรณban hozzรกd, hitelesรญteni tudod ล‘ket. Ez arra is lehetล‘sรฉget ad, hogy automatikus biztonsรกgi mรกsolat kรฉszรผljรถn a kulcsokrรณl. Hitelesรญted a %1 nevลฑ eszkรถzt most? @@ -1076,33 +1236,29 @@ Pรฉlda: https://szerver.em:8787 Elfogadรกs + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 kรผldรถtt egy titkosรญtott รผzenetet - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - * %1 %2 - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message %1 vรกlasza: %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,7 +1289,7 @@ Pรฉlda: https://szerver.em:8787 Nem talรกlhatรณ mikrofon. - + Voice Hang @@ -1164,7 +1320,7 @@ Pรฉlda: https://szerver.em:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. Egy egyedi profil lรฉtrehozรกsa, amellyel be tudsz jelentkezni egyszerre tรถbb fiรณkon keresztรผl รฉs a Nheko tรถbb pรฉldรกnyรกt is tudod futtatni. @@ -1179,21 +1335,37 @@ Pรฉlda: https://szerver.em:8787 profilnรฉv + + ReadReceipts + + + Read receipts + Olvasรกsi jegyek + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username Felhasznรกlรณnรฉv - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. A felhasznรกlรณnรฉv nem lehet รผres รฉs csak a kรถvetkezล‘ karaktereket tartalmazhatja: a-z, 0-9, ., _, =, - รฉs /. - + Password Jelszรณ @@ -1213,7 +1385,7 @@ Pรฉlda: https://szerver.em:8787 Homeszerver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. Egy szerver, amelyen engedรฉlyezve vannak a regisztrรกciรณk. Mivel a Matrix decentralizรกlt, elล‘szรถr talรกlnod kell egy szervert, ahol regisztrรกlhatsz, vagy be kell รกllรญtanod a sajรกt szervered. @@ -1223,27 +1395,17 @@ Pรฉlda: https://szerver.em:8787 REGISZTRรCIร“ - - No supported registration flows! - Nem tรกmogatott regisztrรกciรณs folyamat! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - Egy vagy tรถbb mezล‘ tartalma nem helyes. Kรฉrlek, javรญtsd ki azokat a hibรกkat, รฉs prรณbรกld รบjra! - - - + Autodiscovery failed. Received malformed response. Az automatikus felderรญtรฉs nem sikerรผlt. Helytelen vรกlasz รฉrkezett. - + Autodiscovery failed. Unknown error when requesting .well-known. Az automatikus felderรญtรฉs nem sikerรผlt. Ismeretlen hiba a .well-known lekรฉrรฉse kรถzben. - + The required endpoints were not found. Possibly not a Matrix server. Nem talรกlhatรณk szรผksรฉges vรฉgpontok. Lehet, hogy nem egy Matrixszerver. @@ -1258,17 +1420,17 @@ Pรฉlda: https://szerver.em:8787 Egy ismeretlen hiba tรถrtรฉnt. Ellenล‘rizd, hogy a homeszervered domainje helyes. - + Password is not long enough (min 8 chars) A jelszรณ nem elรฉg hosszรบ (legalรกbb 8 karakter) - + Passwords don't match A jelszavak nem egyeznek - + Invalid server name Nem megfelelล‘ szervernรฉv @@ -1276,7 +1438,7 @@ Pรฉlda: https://szerver.em:8787 ReplyPopup - + Close Bezรกrรกs @@ -1286,10 +1448,28 @@ Pรฉlda: https://szerver.em:8787 Szerkesztรฉs megszakรญtรกsa + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored nincs tรกrolva verziรณ @@ -1297,7 +1477,7 @@ Pรฉlda: https://szerver.em:8787 RoomList - + New tag @@ -1306,16 +1486,6 @@ Pรฉlda: https://szerver.em:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1347,7 +1517,7 @@ Pรฉlda: https://szerver.em:8787 - + Status Message @@ -1367,12 +1537,35 @@ Pรฉlda: https://szerver.em:8787 - + Logout Kijelentkezรฉs - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Bezรกrรกs + + + Start a new chat รšj csevegรฉs indรญtรกsa @@ -1392,7 +1585,7 @@ Pรฉlda: https://szerver.em:8787 Szobรกk jegyzรฉke - + User settings Felhasznรกlรณi beรกllรญtรกsok @@ -1400,12 +1593,12 @@ Pรฉlda: https://szerver.em:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1417,16 +1610,36 @@ Pรฉlda: https://szerver.em:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings Szobabeรกllรญtรกsok - + %1 member(s) %1 tag @@ -1456,7 +1669,12 @@ Pรฉlda: https://szerver.em:8787 Az รถsszes รผzenet - + + Room access + + + + Anyone and guests Bรกrki รฉs vendรฉgek @@ -1471,7 +1689,17 @@ Pรฉlda: https://szerver.em:8787 Meghรญvott felhasznรกlรณk - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption Titkosรญtรกs @@ -1517,12 +1745,12 @@ Pรฉlda: https://szerver.em:8787 Szoba verziรณja - + Failed to enable encryption: %1 Nem sikerรผlt a titkosรญtรกs aktivรกlรกsa: %1 - + Select an avatar Profilkรฉp kivรกlasztรกsa @@ -1542,8 +1770,8 @@ Pรฉlda: https://szerver.em:8787 Hiba a fรกjl olvasรกsa kรถzben: %1 - - + + Failed to upload image: %s Nem sikerรผlt a kรฉp feltรถltรฉse: %s @@ -1551,21 +1779,49 @@ Pรฉlda: https://szerver.em:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1620,6 +1876,121 @@ Pรฉlda: https://szerver.em:8787 Mรฉgse + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1672,18 +2043,18 @@ Pรฉlda: https://szerver.em:8787 TimelineModel - + Message redaction failed: %1 Az รผzenet visszavonรกsa nem sikerรผlt: %1 - + Failed to encrypt event, sending aborted! Nem sikerรผlt titkosรญtani az esemรฉnyt, kรผldรฉs megszakรญtva! - + Save image Kรฉp mentรฉse @@ -1703,7 +2074,7 @@ Pรฉlda: https://szerver.em:8787 Fรกjl mentรฉse - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1711,7 +2082,7 @@ Pรฉlda: https://szerver.em:8787 - + %1 opened the room to the public. %1 nyilvรกnosan elรฉrhetล‘vรฉ tette a szobรกt. @@ -1721,7 +2092,17 @@ Pรฉlda: https://szerver.em:8787 %1 beรกllรญtotta, hogy meghรญvรกssal lehessen csatlakozni ehhez a szobรกhoz. - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. %1 elรฉrhetล‘vรฉ tette a szobรกt vendรฉgeknek. @@ -1741,12 +2122,12 @@ Pรฉlda: https://szerver.em:8787 %1 beรกllรญtotta, hogy a szoba elล‘zmรฉnyei ezentรบl csak a tagok szรกmรกra legyenek lรกthatรณak. - + %1 set the room history visible to members since they were invited. %1 beรกllรญtotta, hogy a szoba elล‘zmรฉnyei lรกthatรณak legyenek a tagok szรกmรกra a meghรญvรกsuktรณl kezdve. - + %1 set the room history visible to members since they joined the room. %1 beรกllรญtotta, hogy a szoba elล‘zmรฉnyei lรกthatรณak legyenek a tagok szรกmรกra a csatlakozรกsuktรณl kezdve. @@ -1756,12 +2137,12 @@ Pรฉlda: https://szerver.em:8787 %1 megvรกltoztatta a szoba engedรฉlyeit. - + %1 was invited. %1 meg lett hรญvva. - + %1 changed their avatar. %1 megvรกltoztatta a profilkรฉpรฉt. @@ -1771,12 +2152,17 @@ Pรฉlda: https://szerver.em:8787 %1 megvรกltoztatta a profiladatait. - + %1 joined. %1 csatlakozott. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. %1 elutasรญtotta a meghรญvรกsรกt. @@ -1806,32 +2192,32 @@ Pรฉlda: https://szerver.em:8787 %1 ki lett tiltva. - + Reason: %1 - + %1 redacted their knock. %1 visszavonta a kopogรกsรกt. - + You joined this room. Csatlakoztรกl ehhez a szobรกhoz. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. Kopogรกs elutasรญtva tล‘le: %1. @@ -1850,7 +2236,7 @@ Pรฉlda: https://szerver.em:8787 TimelineRow - + Edited Szerkesztve @@ -1858,12 +2244,17 @@ Pรฉlda: https://szerver.em:8787 TimelineView - + No room open Nincs nyitott szoba - + + No preview available + + + + %1 member(s) %1 tag @@ -1888,28 +2279,40 @@ Pรฉlda: https://szerver.em:8787 Vissza a szobรกk listรกjรกra - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - Nem talรกlhatรณ titkosรญtott privรกt csevegรฉs ezzel a felhasznรกlรณval. Hozz lรฉtre egy titkosรญtott privรกt csevegรฉst vele, รฉs prรณbรกld รบjra! - - TopBar - + Back to room list Vissza a szobรกk listรกjรกra - + No room selected Nincs kivรกlasztva szoba - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options Szoba beรกllรญtรกsai @@ -1947,10 +2350,35 @@ Pรฉlda: https://szerver.em:8787 Kilรฉpรฉs + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile Globรกlis felhasznรกlรณi profil @@ -1960,33 +2388,98 @@ Pรฉlda: https://szerver.em:8787 Szobai felhasznรกlรณi profil - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify Hitelesรญtรฉs - - Ban the user - A felhasznรกlรณ tiltรกsa - - - - Start a private chat - Privรกt csevegรฉs indรญtรกsa + + Start a private chat. + - Kick the user - A felhasznรกlรณ kirรบgรกsa + Kick the user. + - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify Hitelesรญtรฉs visszavonรกsa - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar Profilkรฉp kivรกlasztรกsa @@ -2009,8 +2502,8 @@ Pรฉlda: https://szerver.em:8787 UserSettings - - + + Default Alapรฉrtelmezett @@ -2018,7 +2511,7 @@ Pรฉlda: https://szerver.em:8787 UserSettingsPage - + Minimize to tray Kicsinyรญtรฉs a tรกlcรกra @@ -2028,22 +2521,22 @@ Pรฉlda: https://szerver.em:8787 Indรญtรกs a tรกlcรกn - + Group's sidebar Csoport oldalsรกvja - + Circular Avatars Kerekรญtett profilkรฉpek - + profile: %1 profil: %1 - + Default Alapรฉrtelmezett @@ -2068,7 +2561,7 @@ Pรฉlda: https://szerver.em:8787 LETร–LTร‰S - + Keep the application running in the background after closing the client window. Az alkalmazรกs azutรกn is a hรกttรฉrben fut, miutรกn be lett zรกrva a fล‘ablak. @@ -2084,6 +2577,16 @@ OFF - square, ON - Circle. A profilkรฉpek megjelenรฉse a csevegรฉsekben. KI - szรถgletes, BE - kerek. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2114,7 +2617,7 @@ be blurred. az idล‘vonal homรกlyosรญtva lesz. - + Privacy screen timeout (in seconds [0 - 3600]) Idล‘vonal kitakarรกsa ennyi idล‘ utรกn (mรกsodpercben, 0 รฉs 3600 kรถzรถtt) @@ -2175,7 +2678,7 @@ Ha ki van kapcsolva, a szobรกk sorrendje csak a bennรผk lรฉvล‘ utolsรณ รผzenet d Ha be van kapcsolva, azok a szobรกk kerรผlnek felรผlre, amelyekhez aktรญv รฉrtesรญtรฉs tartozik (amelyet a szรกmot tartalmazรณ kis kรถr jelez). A nรฉmรญtott szobรกk tovรกbbra is dรกtum alapjรกn lesznek rendezve, mivel nem valรณszรญnลฑ, hogy ezeket annyira fontosnak tartod, mint a tรถbbi szobรกt. - + Read receipts Olvasรกsi jegyek @@ -2187,7 +2690,7 @@ Status is displayed next to timestamps. Ez az รกllapot az รผzenetek ideje mellett jelenik meg. - + Send messages as Markdown รœzenetek kรผldรฉse Markdownkรฉnt @@ -2200,6 +2703,11 @@ Ha ki van kapcsolva, az รถsszes รผzenet sima szรถvegkรฉnt lesz elkรผldve. + Play animated images only on hover + + + + Desktop notifications Asztali รฉrtesรญtรฉsek @@ -2241,12 +2749,47 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ A betลฑmรฉret megnรถvelรฉse, ha az รผzenetek csak nรฉhรกny hangulatjelet tartalmaznak. - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices Kulcsok megosztรกsa hitelesรญtett felhasznรกlรณkkal รฉs eszkรถzรถkkel - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED GYORSรTร“TรRAZVA @@ -2256,7 +2799,7 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ NINCS GYORSรTร“TรRAZVA - + Scale factor Nagyรญtรกsi tรฉnyezล‘ @@ -2331,7 +2874,7 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ Eszkรถzujjlenyomat - + Session Keys Munkamenetkulcsok @@ -2351,17 +2894,22 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ TITKOSรTรS - + GENERAL รLTALรNOS - + INTERFACE FELรœLET - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode ร‰rintล‘ kรฉpernyล‘s mรณd @@ -2376,12 +2924,7 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ Hangulatjelek betลฑtรญpusa - - Automatically replies to key requests from other users, if they are verified. - Automatikus vรกlasz a mรกs felhasznรกlรณktรณl รฉrkezล‘ kulcskรฉrelmekre, ha ล‘k hitelesรญtve vannak. - - - + Master signing key Mester-alรกรญrรณkulcs @@ -2401,7 +2944,7 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ A mรกsok hitelesรญtรฉsรฉre hasznรกlt kulcs. Ha gyorsรญtรณtรกrazva van, egy felhasznรกlรณ hitelesรญtรฉsekor hitelesรญtve lesz az รถsszes eszkรถze. - + Self signing key ร–nalรกรญrรณkulcs @@ -2431,14 +2974,14 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ Minden fรกjl (*) - + Open Sessions File Munkameneti fรกjl megnyitรกsa - + @@ -2446,19 +2989,19 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ Hiba - - + + File Password Fรกjljelszรณ - + Enter the passphrase to decrypt the file: รrd be a jelmondatot a fรกjl titkosรญtรกsรกnak feloldรกsรกhoz: - + The password cannot be empty A jelszรณ nem lehet รผres @@ -2473,6 +3016,14 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ Exportรกlt munkameneti kulcsok mentรฉse fรกjlba + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Nem talรกlhatรณ titkosรญtott privรกt csevegรฉs ezzel a felhasznรกlรณval. Hozz lรฉtre egy titkosรญtott privรกt csevegรฉst vele, รฉs prรณbรกld รบjra! + + Waiting @@ -2598,37 +3149,6 @@ Ettล‘l รกltalรกban animรกlttรก vรกlik az alkalmazรกsablakok listรกjรกn szereplล‘ Nyisd meg a fallback-ket, kรถvesd az utasรญtรกsokat, รฉs erล‘sรญtsd meg, ha vรฉgeztรฉl velรผk! - - dialogs::JoinRoom - - - Join - Csatlakozรกs - - - - Cancel - Mรฉgse - - - - Room ID or alias - Szoba azonosรญtรณja vagy รกlneve - - - - dialogs::LeaveRoom - - - Cancel - Mรฉgse - - - - Are you sure you want to leave? - Biztosan tรกvozni akarsz? - - dialogs::Logout @@ -2682,32 +3202,6 @@ Mรฉdia mรฉrete: %2 Oldd meg a reCAPTCHA feladvรกnyรกt, รฉs nyomd meg a โ€žMegerล‘sรญtรฉsโ€ gombot - - dialogs::ReadReceipts - - - Read receipts - Olvasรกsi jegyek - - - - Close - Bezรกrรกs - - - - dialogs::ReceiptItem - - - Today %1 - Ma %1 - - - - Yesterday %1 - Tegnap %1 - - message-description sent: @@ -2721,47 +3215,47 @@ Mรฉdia mรฉrete: %2 %1 kรผldรถtt egy hangfรกjlt - + You sent an image Kรผldtรฉl egy kรฉpet - + %1 sent an image %1 kรผldรถtt egy kรฉpet - + You sent a file Kรผldtรฉl egy fรกjlt - + %1 sent a file %1 kรผldtรฉl egy fรกjlt - + You sent a video Kรผldtรฉl egy videรณt - + %1 sent a video %1 kรผldรถtt egy videรณt - + You sent a sticker Kรผldtรฉl egy matricรกt - + %1 sent a sticker %1 kรผldรถtt egy matricรกt - + You sent a notification Kรผldtรฉl egy รฉrtesรญtรฉst @@ -2776,7 +3270,7 @@ Mรฉdia mรฉrete: %2 Te: %1 - + %1: %2 %1: %2 @@ -2796,27 +3290,27 @@ Mรฉdia mรฉrete: %2 Hรญvรกst kezdemรฉnyeztรฉl - + %1 placed a call %1 hรญvรกst kezdemรฉnyezett - + You answered a call Fogadtรกl egy hรญvรกst - + %1 answered a call %1 hรญvรกst fogadott - + You ended a call Befejeztรฉl egy hรญvรกst - + %1 ended a call %1 befejezett egy hรญvรกst @@ -2824,7 +3318,7 @@ Mรฉdia mรฉrete: %2 utils - + Unknown Message Type Ismeretlen รผzenettรญpus diff --git a/resources/langs/nheko_id.ts b/resources/langs/nheko_id.ts new file mode 100644 index 00000000..accb1eab --- /dev/null +++ b/resources/langs/nheko_id.ts @@ -0,0 +1,3325 @@ + + + + + ActiveCallBar + + + Calling... + Memanggil... + + + + + Connecting... + Menghubungkan... + + + + You are screen sharing + Anda sedang membagikan layar + + + + Hide/Show Picture-in-Picture + Sembunyikan/Tampilkan Picture-in-Picture + + + + Unmute Mic + Bunyikan Mikrofon + + + + Mute Mic + Bisukan Mikrofon + + + + AwaitingVerificationConfirmation + + + Awaiting Confirmation + Menunggu Konfirmasi + + + + Waiting for other side to complete verification. + Menunggu untuk pengguna yang lain untuk menyelesaikan verifikasi. + + + + Cancel + Batal + + + + CallInvite + + + Video Call + Panggilan Video + + + + Voice Call + Panggilan Suara + + + + No microphone found. + Tidak ada mikrofon yang ditemukan. + + + + CallInviteBar + + + Video Call + Panggilan Video + + + + Voice Call + Panggilan Suara + + + + Devices + Perangkat + + + + Accept + Terima + + + + Unknown microphone: %1 + Mikrofon tidak dikenal: %1 + + + + Unknown camera: %1 + Kamera tidak dikenal: %1 + + + + Decline + Tolak + + + + No microphone found. + Tidak ada mikrofon yang ditemukan. + + + + CallManager + + + Entire screen + Semua layar + + + + ChatPage + + + Failed to invite user: %1 + Gagal mengundang pengguna: %1 + + + + + Invited user: %1 + Pengguna yang diundang: %1 + + + + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. + Migrasi cache ke versi saat ini gagal. Ini dapat memiliki alasan yang berbeda. Silakan buka masalah dan coba gunakan versi yang lebih lama untuk sementara. Alternatifnya Anda dapat mencoba menghapus cache secara manual. + + + + Confirm join + Konfirmasi untuk bergabung + + + + Do you really want to join %1? + Apakah Anda ingin bergabung %1? + + + + Room %1 created. + Ruangan %1 telah dibuat. + + + + + Confirm invite + Konfirmasi undangan + + + + Do you really want to invite %1 (%2)? + Apakah Anda ingin menundang %1 (%2)? + + + + Failed to invite %1 to %2: %3 + Gagal mengundang %1 ke %2: %3 + + + + Confirm kick + Konfirmasi pengeluaran + + + + Do you really want to kick %1 (%2)? + Apakah Anda ingin mengeluarkan %1 (%2)? + + + + Kicked user: %1 + Pengguna yang dikeluarkan: %1 + + + + Confirm ban + Konfirmasi cekalan + + + + Do you really want to ban %1 (%2)? + Apakah Anda ingin mencekal %1 (%2)? + + + + Failed to ban %1 in %2: %3 + Gagal mencekal %1 di %2: %3 + + + + Banned user: %1 + Pengguna yang dicekal: %1 + + + + Confirm unban + Konfirmasi menghilangkan cekalan + + + + Do you really want to unban %1 (%2)? + Apakah Anda ingin menghilangkan cekalan %1 (%2)? + + + + Failed to unban %1 in %2: %3 + Gagal menghilangkan pencekalan %1 di %2: %3 + + + + Unbanned user: %1 + Menghilangkan cekalan pengguna: %1 + + + + Do you really want to start a private chat with %1? + Apakah Anda ingin memulai chat privat dengan %1? + + + + Cache migration failed! + Migrasi cache gagal! + + + + Incompatible cache version + Versi cache tidak kompatibel + + + + The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache. + Cache pada disk Anda lebih baru daripada versi yang didukung Nheko ini. Harap perbarui atau kosongkan cache Anda. + + + + Failed to restore OLM account. Please login again. + Gagal memulihkan akun OLM. Mohon masuk lagi. + + + + + + Failed to restore save data. Please login again. + Gagal memulihkan data simpanan. Mohon masuk lagi. + + + + Failed to setup encryption keys. Server response: %1 %2. Please try again later. + Gagal menyiapkan kunci enkripsi. Respons server: %1 %2. Silakan coba lagi nanti. + + + + + Please try to login again: %1 + Mohon mencoba masuk lagi: %1 + + + + Failed to join room: %1 + Gagal bergabung ruangan: %1 + + + + You joined the room + Anda bergabung ruangan ini + + + + Failed to remove invite: %1 + Gagal menghapus undangan: %1 + + + + Room creation failed: %1 + Pembuatan ruangan gagal: %1 + + + + Failed to leave room: %1 + Gagal meninggalkan ruangan: %1 + + + + Failed to kick %1 from %2: %3 + Gagal mengeluarkan %1 dari %2: %3 + + + + CommunitiesList + + + Hide rooms with this tag or from this space by default. + Sembunyikan ruangan dengan tanda ini atau dari space ini secara default. + + + + CommunitiesModel + + + All rooms + Semua ruangan + + + + Shows all rooms without filtering. + Menampilkan semua ruangan tanpa penyaringan. + + + + Favourites + Favorit + + + + Rooms you have favourited. + Ruangan yang Anda favoritkan. + + + + Low Priority + Prioritas Rendah + + + + Rooms with low priority. + Ruangan dengan prioritas rendah. + + + + Server Notices + Pemberitahuan Server + + + + Messages from your server or administrator. + Pesan dari server Anda atau administrator. + + + + CrossSigningSecrets + + + Decrypt secrets + Dekripsi rahasia + + + + Enter your recovery key or passphrase to decrypt your secrets: + Masukkan kunci pemulihan Anda atau frasa sandi untuk mendekripsikan rahasia Anda: + + + + Enter your recovery key or passphrase called %1 to decrypt your secrets: + Masukkan kunci pemulihan Anda atau frasa sandi yang bernama %1 untuk mendekripsikan rahasia Anda: + + + + Decryption failed + Gagal mendekripsi + + + + Failed to decrypt secrets with the provided recovery key or passphrase + Gagal mendekripsi rahasia dengan kunci pemulihan atau frasa sandi yang diberikan + + + + DigitVerification + + + Verification Code + Kode Verifikasi + + + + Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification! + Harap verifikasi digit berikut. Anda seharusnya melihat angka yang sama di kedua sisi. Jika mereka berbeda, mohon tekan 'Mereka tidak cocok!' untuk membatalkan verifikasi! + + + + They do not match! + Mereka tidak cocok! + + + + They match! + Mereka cocok! + + + + EditModal + + + Apply + Terapkan + + + + Cancel + Batalkan + + + + Name + Nama + + + + Topic + Topik + + + + EmojiPicker + + + Search + Cari + + + + People + Orang + + + + Nature + Alam + + + + Food + Makanan + + + + Activity + Aktifitas + + + + Travel + Tempat + + + + Objects + Objek + + + + Symbols + Simbol + + + + Flags + Bendera + + + + EmojiVerification + + + Verification Code + Kode Verifikasi + + + + Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification! + Mohon verifikasi emoji berikut. Anda seharusnya melihat emoji yang sama di kedua sisi. Jika mereka berbeda, mohon tekan 'Mereka tidak cocok!' untuk membatalkan verifikasi! + + + + They do not match! + Mereka tidak cocok! + + + + They match! + Mereka cocok! + + + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Tidak ada kunci untuk mengakses pesan ini. Kami telah meminta untuk kunci secara otomatis, tetapi Anda dapat meminta lagi jika Anda tidak sabar. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Pesan ini tidak dapat didekripsikan, karena kami hanya memiliki kunci untuk pesan baru. Anda dapat meminta akses ke pesan ini. + + + + There was an internal error reading the decryption key from the database. + Sebuah kesalahan internal terjadi saat membaca kunci dekripsi dari basis data. + + + + There was an error decrypting this message. + Sebuah error terjadi saat mendekripsikan pesan ini. + + + + The message couldn't be parsed. + Pesan ini tidak dapat diuraikan. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + Kunci enkripsi telah digunakan lagi! Seseorang mungkin mencoba memasukkan pesan palsu ke chat ini! + + + + Unknown decryption error + Error dekripsi yang tidak dikenal + + + + Request key + Minta kunci + + + + EncryptionIndicator + + + This message is not encrypted! + Pesan ini tidak terenkripsi! + + + + Encrypted by a verified device + Terenkripsi oleh perangkat yang terverifikasi + + + + Encrypted by an unverified device, but you have trusted that user so far. + Terenkripsi oleh perangkat yang tidak diverifikasi, tetapi Anda mempercayai pengguna itu sejauh ini. + + + + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Terenkripsi oleh perangkat yang tidak diverifikasi atau kuncinya dari sumber yang tidak dipercayai seperti cadangan kunci. + + + + Failed + + + Verification failed + Verifikasi gagal + + + + Other client does not support our verification protocol. + Client yang lain tidak mendukung protokol verifikasi kami. + + + + Key mismatch detected! + Ketidakcocokan kunci terdeteksi! + + + + Device verification timed out. + Waktu verifikasi perangkat habis. + + + + Other party canceled the verification. + Pengguna yang lain membatalkan proses verifikasi ini. + + + + Verification messages received out of order! + + + + + Unknown verification error. + + + + + Close + Tutup + + + + ForwardCompleter + + + Forward Message + Teruskan Pesan + + + + ImagePackEditorDialog + + + Editing image pack + Mengedit paket gambar + + + + Add images + Tambahkan gambar + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Stiker (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + Kunci keadaan + + + + Packname + Nama Paket + + + + Attribution + Atribusi + + + + + Use as Emoji + Gunakan sebagai Emoji + + + + + Use as Sticker + Gunakan sebagai Stiker + + + + Shortcode + Kode Pendek + + + + Body + Body + + + + Remove from pack + Hapus dari paket + + + + Remove + Hapus + + + + Cancel + Batalkan + + + + Save + Simpan + + + + ImagePackSettingsDialog + + + Image pack settings + Pengaturan paket gambar + + + + Create account pack + Buat paket untuk akun + + + + New room pack + Paket ruangan baru + + + + Private pack + Paket privat + + + + Pack from this room + Paket dari ruangan ini + + + + Globally enabled pack + Paket yang diaktifkan secara global + + + + Enable globally + Aktifkan secara global + + + + Enables this pack to be used in all rooms + Mengaktifkan paket ini untuk digunakan di semua ruangan + + + + Edit + Edit + + + + Close + Tutup + + + + InputBar + + + Select a file + Pilih sebuah file + + + + All Files (*) + Semua File (*) + + + + Failed to upload media. Please try again. + Gagal untuk mengunggah media. Silakan coba lagi. + + + + InviteDialog + + + Invite users to %1 + Undang pengguna ke %1 + + + + User ID to invite + ID Pengguna untuk diundang + + + + @joe:matrix.org + Example user id. The name 'joe' can be localized however you want. + @pengguna:matrix.org + + + + Add + Tambahkan + + + + Invite + Undang + + + + Cancel + Batalkan + + + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + ID ruangan atau alias + + + + LeaveRoomDialog + + + Leave room + Tinggalkan ruangan + + + + Are you sure you want to leave? + Apakah Anda yakin untuk meninggalkan ruangan? + + + + LoginPage + + + Matrix ID + ID Matrix + + + + e.g @joe:matrix.org + mis. @pengguna:matrix.org + + + + Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :. +You can also put your homeserver address there, if your server doesn't support .well-known lookup. +Example: @user:server.my +If Nheko fails to discover your homeserver, it will show you a field to enter the server manually. + Nama login Anda. Sebuah MXID harus mulai dengan @ diikuti dengan ID pengguna. Setelah ID penggunanya Anda harus menambahkan nama server setelah :. +Anda juga dapat memasukkan alamat homeserver Anda, jika server Anda tidak mendukung pencarian .well-known. +Misalnya: @pengguna:server.my +Jika Nheko gagal menemukan homeserver Anda, Nheko akan menampilkan kolom untuk memasukkan servernya secara manual. + + + + Password + Kata Sandi + + + + Your password. + Kata sandi Anda. + + + + Device name + Nama perangkat + + + + A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used. + Sebuah nama perangkat untuk perangkat ini, yang akan ditampilkan untuk yang lain, ketika memverifikasi perangkat Anda. Jika tidak dimasukkan nama perangkat yang default akan digunakan. + + + + Homeserver address + Alamat homeserver + + + + server.my:8787 + server.my:8787 + + + + The address that can be used to contact you homeservers client API. +Example: https://server.my:8787 + Alamat yang dapat digunakan untuk menghubungi API klien homeserver Anda. +Misalnya: https://server.my:8787 + + + + LOGIN + MASUK + + + + + + + You have entered an invalid Matrix ID e.g @joe:matrix.org + Anda telah memasukkan ID Matrix yang tidak valid mis. @pengguna:matrix.org + + + + Autodiscovery failed. Received malformed response. + Penemuan otomatis gagal. Menerima respons cacat. + + + + Autodiscovery failed. Unknown error when requesting .well-known. + Penemuan otomatis gagal. Kesalahan yang tidak diketahu saat meminta .well-known. + + + + The required endpoints were not found. Possibly not a Matrix server. + Titik akhir yang dibutuhkan tidak dapat ditemukan. Kemungkinan bukan server Matrix. + + + + Received malformed response. Make sure the homeserver domain is valid. + Menerima respons cacat. Pastikan domain homeservernya valid. + + + + An unknown error occured. Make sure the homeserver domain is valid. + Terjadi kesalahan yang tidak diketahui. Pastikan domain homeservernya valid. + + + + SSO LOGIN + LOGIN SSO + + + + Empty password + Kata sandi kosong + + + + SSO login failed + Login SSO gagal + + + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + + + MessageDelegate + + + Encryption enabled + Enkripsi diaktifkan + + + + room name changed to: %1 + nama ruangan diganti ke: %1 + + + + removed room name + nama ruangan dihapus + + + + topic changed to: %1 + topik diganti ke: %1 + + + + removed topic + topik dihapus + + + + %1 changed the room avatar + %1 mengubah avatar ruangan + + + + %1 created and configured room: %2 + %1 membuat dan mengkonfigurasikan ruangan: %2 + + + + %1 placed a voice call. + %1 melakukan panggilan suara. + + + + %1 placed a video call. + %1 melakukan panggilan suara. + + + + %1 placed a call. + %1 melakukan panggilan. + + + + Negotiating call... + Negosiasi panggilanโ€ฆ + + + + Allow them in + Izinkan mereka untuk masuk + + + + %1 answered the call. + %1 menjawab panggilan. + + + + + removed + dihapus + + + + %1 ended the call. + %1 mengakhir panggilan. + + + + MessageInput + + + Hang up + Tutup panggilan + + + + Place a call + Lakukan panggilan + + + + Send a file + Kirim sebuah file + + + + Write a message... + Ketik pesanโ€ฆ + + + + Stickers + Stiker + + + + Emoji + Emoji + + + + Send + Kirim + + + + You don't have permission to send messages in this room + Anda tidak memiliki izin untuk mengirim pesan di ruangan ini + + + + MessageView + + + Edit + Edit + + + + React + Reaksi + + + + Reply + Balas + + + + Options + Opsi + + + + + &Copy + &Salin + + + + + Copy &link location + Salin lokasi &tautan + + + + Re&act + Re&aksi + + + + Repl&y + Bala&s + + + + &Edit + &Edit + + + + Read receip&ts + Lapor&an terbaca + + + + &Forward + &Teruskan + + + + &Mark as read + &Tandai sebagai dibaca + + + + View raw message + Tampilkan pesan mentah + + + + View decrypted raw message + Tampilkan pesan terdekripsi mentah + + + + Remo&ve message + Hap&us pesan + + + + &Save as + &Simpan sebagai + + + + &Open in external program + &Buka di program eksternal + + + + Copy link to eve&nt + Salin tautan ke peristi&wa + + + + &Go to quoted message + &Pergi ke pesan yang dikutip + + + + NewVerificationRequest + + + Send Verification Request + Kirim Permintaan Verifikasi + + + + Received Verification Request + Menerima Permintaan Verifikasi + + + + 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.) + + + + + 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? + Untuk mengizinkan pengguna yang lain untuk melihat, perangkat apa saja yang sebenarnya milik Anda, verifiksi mereka. Ini juga dapat membuat kunci cadangan bekerja secara otomatis. Verifikasi %1 sekarang? + + + + To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party. + Supaya tidak ada pengguna yang jahat yang bisa melihat komunikasi yang terenkripsi Anda dapat memverifikasi pengguna yang lain. + + + + %1 has requested to verify their device %2. + %1 telah meminta untuk memverifikasi perangkat %2 mereka. + + + + %1 using the device %2 has requested to be verified. + %1 yang menggunakan perangkat %2 meminta untuk diverifikasi. + + + + Your device (%1) has requested to be verified. + Perangkat Anda (%1) meminta untuk diverifikasi. + + + + Cancel + Batalkan + + + + Deny + Tolak + + + + Start verification + Mulai verifikasi + + + + Accept + Terima + + + + NotificationWarning + + + You are about to notify the whole room + + + + + NotificationsManager + + + + + %1 sent an encrypted message + %1 mengirim pesan terenkripsi + + + + %1 replied: %2 + Format a reply in a notification. %1 is the sender, %2 the message + %1 membalas: %2 + + + + + %1 replied with an encrypted message + %1 membalas dengan pesan terenkripsi + + + + %1 replied to a message + %1 membalas pesan + + + + %1 sent a message + %1 mengirim gambar + + + + PlaceCall + + + Place a call to %1? + Lakukan panggilan ke %1? + + + + No microphone found. + Tidak ada mikrofon yang ditemukan. + + + + Voice + Suara + + + + Video + Video + + + + Screen + Layar + + + + Cancel + Batalkan + + + + Placeholder + + + unimplemented event: + peristiwa yang belum diimplementasikan: + + + + QCoreApplication + + + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. + Membuat profil yang unik, yang mengizinkan Anda untuk masuk ke beberapa akun pada waktu bersamaan dan mulai beberapa instansi nheko. + + + + profile + profil + + + + profile name + nama profil + + + + ReadReceipts + + + Read receipts + Laporan dibaca + + + + ReadReceiptsModel + + + Yesterday, %1 + Kemarin, %1 + + + + RegisterPage + + + Username + Nama pengguna + + + + + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. + Nama pengguna tidak boleh kosong, dan hanya mengandung karakter a-z, 0-9, ., _, =, -, dan /. + + + + Password + Kata sandi + + + + Please choose a secure password. The exact requirements for password strength may depend on your server. + Mohon memilih kata sandi yang aman. Persyaratan untuk kekuatan sandi mungkin bergantung pada server Anda. + + + + Password confirmation + Konfirmasi kata sandi + + + + Homeserver + Homeserver + + + + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. + Sebuah server yang mengizinkan pendaftaran. Karena Matrix itu terdecentralisasi, Anda pertama harus mencari server yang Anda daftar atau host server Anda sendiri. + + + + REGISTER + DAFTAR + + + + Autodiscovery failed. Received malformed response. + Penemuan otomatis gagal. Menerima respons cacat. + + + + Autodiscovery failed. Unknown error when requesting .well-known. + Penemuan otomatis gagal. Terjadi kesalahan yang tidak diketahui saat meminta .well-known. + + + + The required endpoints were not found. Possibly not a Matrix server. + Titik akhir yang dibutuhkan tidak dapat ditemukan. Kemungkinan bukan server Matrix. + + + + Received malformed response. Make sure the homeserver domain is valid. + Menerima respons cacat. Pastikan domain homeservernya valid. + + + + An unknown error occured. Make sure the homeserver domain is valid. + Terjadi kesalahan yang tidak diketahui. Pastikan domain homeservernya valid. + + + + Password is not long enough (min 8 chars) + Kata sandi kurang panjang (min. 8 karakter) + + + + Passwords don't match + Kata sandi tidak cocok + + + + Invalid server name + Nama server tidak valid + + + + ReplyPopup + + + Close + Tutup + + + + Cancel edit + Batalkan pengeditan + + + + RoomDirectory + + + Explore Public Rooms + Temukan Ruangan Publik + + + + Search for public rooms + Cari ruangan publik + + + + Choose custom homeserver + + + + + RoomInfo + + + no version stored + tidak ada versi yang disimpan + + + + RoomList + + + New tag + Tanda baru + + + + Enter the tag you want to use: + Masukkan tanda yang Anda ingin gunakan: + + + + Leave room + Tinggalkan ruangan + + + + Tag room as: + Tandai ruangan sebagai: + + + + Favourite + Favorit + + + + Low priority + Prioritas rendah + + + + Server notice + Pemberitahuan server + + + + Create new tag... + Membuat tanda baruโ€ฆ + + + + Status Message + Pesan Status + + + + Enter your status message: + Masukkan pesan status Anda: + + + + Profile settings + Pengaturan profil + + + + Set status message + Tetapkan pesan status + + + + Logout + Keluar + + + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Tutup + + + + Start a new chat + Mulai chat baru + + + + Join a room + Bergabung sebuah ruangan + + + + Create a new room + Buat ruangan baru + + + + Room directory + Direktori ruangan + + + + User settings + Pengaturan pengguna + + + + RoomMembers + + + Members of %1 + Anggota dari %1 + + + + %n people in %1 + Summary above list of members + + %n orang di %1 + + + + + Invite more people + Undang banyak orang + + + + This room is not encrypted! + Ruangan ini tidak terenkripsi! + + + + This user is verified. + Pengguna ini sudah diverifikasi. + + + + This user isn't verified, but is still using the same master key from the first time you met. + Pengguna ini belum diverifikasi, tetapi masih menggunakan kunci utama dari pertama kali Anda bertemu. + + + + This user has unverified devices! + Pengguna ini memiliki perangkat yang belum diverifikasi! + + + + RoomSettings + + + Room Settings + Pengaturan Ruangan + + + + %1 member(s) + %1 anggota + + + + SETTINGS + PENGATURAN + + + + Notifications + Notifikasi + + + + Muted + Bisukan + + + + Mentions only + Sebutan saja + + + + All messages + Semua pesan + + + + Room access + Akses ruangan + + + + Anyone and guests + Siapa saja dan tamu + + + + Anyone + Siapa saja + + + + Invited users + Pengguna yang diundang + + + + By knocking + Dengan mengetuk + + + + Restricted by membership in other rooms + Dibatasi oleh keanggotaan di ruangan lain + + + + Encryption + Enkripsi + + + + End-to-End Encryption + Enkripsi Ujung-ke-Ujung + + + + Encryption is currently experimental and things might break unexpectedly. <br> + Please take note that it can't be disabled afterwards. + Enkripsi saat ini masih eksperimental dan apapun dapat rusak secara tiba-tiba.<br>Mohon dicatat bahwa enkripsi tidak dapat dinonaktifkan setelah ini. + + + + Sticker & Emote Settings + Pengaturan Stiker & Emote + + + + Change + Ubah + + + + Change what packs are enabled, remove packs or create new ones + Ubah paket apa yang diaktifkan, hapus paket atau buat yang baru + + + + INFO + INFO + + + + Internal ID + ID Internal + + + + Room Version + Versi Ruangan + + + + Failed to enable encryption: %1 + Gagal mengaktifkan enkripsi: %1 + + + + Select an avatar + Pilih sebuah avatar + + + + All Files (*) + Semua File (*) + + + + The selected file is not an image + File yang dipilih bukan sebuah gambar + + + + Error while reading file: %1 + Terjadi kesalahan saat membaca file: %1 + + + + + Failed to upload image: %s + Gagal mengunggah gambar: %s + + + + RoomlistModel + + + Pending invite. + Undangan tertunda. + + + + Previewing this room + Menampilkan ruangan ini + + + + No preview available + Tidak ada tampilan yang tersedia + + + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + + + ScreenShare + + + Share desktop with %1? + Bagikan desktop dengan %1? + + + + Window: + Jendela: + + + + Frame rate: + Frame rate: + + + + Include your camera picture-in-picture + Tambahkan kamera Anda dalam picture-in-picture + + + + Request remote camera + Minta kamera jarak jauh + + + + + View your callee's camera like a regular video call + Tampilkan kamera pengguna yang menerima panggilan seperti panggilan video biasa + + + + Hide mouse cursor + Sembunyikan kursor mouse + + + + Share + Bagikan + + + + Preview + Tampilkan + + + + Cancel + Batalkan + + + + SecretStorage + + + Failed to connect to secret storage + Gagal menghubungkan ke penyimpanan rahasia + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Gagal memperbarui paket gambar: %1 + + + + Failed to delete old image pack: %1 + Gagal menghapus paket gambar yang lama: %1 + + + + Failed to open image: %1 + Gagal membuka gambar: %1 + + + + Failed to upload image: %1 + Gagal mengunggah gambar: %1 + + + + StatusIndicator + + + Failed + Gagal + + + + Sent + Terkirim + + + + Received + Diterima + + + + Read + Dibaca + + + + StickerPicker + + + Search + Cari + + + + Success + + + Successful Verification + Verifikasi Berhasil + + + + Verification successful! Both sides verified their devices! + Verifikasi berhasil! Kedua sisi telah memverifikasi perangkat mereka! + + + + Close + Tutup + + + + TimelineModel + + + Message redaction failed: %1 + Reaksi pesan gagal: %1 + + + + + Failed to encrypt event, sending aborted! + Gagal mendekripsikan peristiwa, pengiriman dihentikan! + + + + Save image + Simpan gambar + + + + Save video + Simpan video + + + + Save audio + Simpan audio + + + + Save file + Simpan file + + + + %1 and %2 are typing. + Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) + + %1 dan %2 sedang mengetik. + + + + + %1 opened the room to the public. + %1 membuka ruangan ke publik. + + + + %1 made this room require and invitation to join. + %1 membuat ruangan ini membutuhkan undangan untuk bergabung. + + + + %1 allowed to join this room by knocking. + %1 mengizinkan siapa saja untuk bergabung ke ruangan ini dengan mengetuk. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 mengizinkan anggota dari ruangan berikut untuk bergabung ke ruangan ini secara otomatis: %2 + + + + %1 made the room open to guests. + %1 membuat ruangan ini terbuka ke tamu. + + + + %1 has closed the room to guest access. + %1 telah menutup ruangan ke akses tamu. + + + + %1 made the room history world readable. Events may be now read by non-joined people. + %1 membuat sejarah ruangan dibaca oleh siapa saja. Peristiwa mungkin bisa dibaca oleh orang yang tidak bergabung. + + + + %1 set the room history visible to members from this point on. + %1 membuat sejarah ruangan bisa dilihat oleh anggota dari titik sekarang. + + + + %1 set the room history visible to members since they were invited. + %1 membuat sejarah ruangan bisa dilihat oleh anggota yang telah diundang. + + + + %1 set the room history visible to members since they joined the room. + %1 membuat sejarah ruangan bisa dilihat oleh anggota yang telah bergabung ke ruangan ini. + + + + %1 has changed the room's permissions. + %1 telah mengubah izin ruangan. + + + + %1 was invited. + %1 diundang. + + + + %1 changed their avatar. + %1 mengubah avatarnya. + + + + %1 changed some profile info. + %1 mengubah info profil. + + + + %1 joined. + %1 bergabung. + + + + %1 joined via authorisation from %2's server. + %1 bergabung via otorisasi dari servernya %2. + + + + %1 rejected their invite. + %1 menolak undangannya. + + + + Revoked the invite to %1. + Menghapus undangan ke %1. + + + + %1 left the room. + %1 meninggalkan ruangan. + + + + Kicked %1. + %1 dikeluarkan. + + + + Unbanned %1. + Cekalan %1 dihilangkan. + + + + %1 was banned. + %1 telah dicekal. + + + + Reason: %1 + Alasan: %1 + + + + %1 redacted their knock. + %1 menolak ketukannya. + + + + You joined this room. + Anda bergabung ruangan ini. + + + + %1 has changed their avatar and changed their display name to %2. + %1 mengubah avatarnya dan ubah nama tampilannya ke %2. + + + + %1 has changed their display name to %2. + %1 mengubah nama tampilannya ke %2. + + + + Rejected the knock from %1. + Menolak ketukan dari %1. + + + + %1 left after having already left! + This is a leave event after the user already left and shouldn't happen apart from state resets + %1 keluar setelah sudah keluar! + + + + %1 knocked. + %1 mengetuk. + + + + TimelineRow + + + Edited + Diedit + + + + TimelineView + + + No room open + Tidak ada ruangan yang dibuka + + + + No preview available + Tidak ada tampilan yang tersedia + + + + %1 member(s) + %1 anggota + + + + join the conversation + bergabung ke percakapan + + + + accept invite + terima undangan + + + + decline invite + tolak undangan + + + + Back to room list + Kembali ke daftar ruangan + + + + TopBar + + + Back to room list + Kembali ke daftar ruangan + + + + No room selected + Tidak ada ruangan yang dipilih + + + + This room is not encrypted! + Ruangan ini tidak dienkripsi! + + + + This room contains only verified devices. + Ruangan ini hanya berisi perangkat yang telah diverifikasi. + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + Ruangan ini berisi perangkat yang belum diverifikasi! + + + + Room options + Opsi ruangan + + + + Invite users + Undang pengguna + + + + Members + Anggota + + + + Leave room + Tinggalkan ruangan + + + + Settings + Pengaturan + + + + TrayIcon + + + Show + Tampilkan + + + + Quit + Tutup + + + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Mohon masukkan token pendaftaran yang valid. + + + + Invalid token + + + + + UserProfile + + + Global User Profile + Profil Pengguna Global + + + + Room User Profile + Profil Pengguna di Ruangan + + + + Change avatar globally. + Ubah avatar secara global. + + + + Change avatar. Will only apply to this room. + Ubah avatar. Hanya diterapkan di ruangan ini. + + + + Change display name globally. + Ubah nama tampilan secara global. + + + + Change display name. Will only apply to this room. + Ubah nama tampilan. Hanya diterapkan di ruangan ini. + + + + Room: %1 + Ruangan: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + Ini adalah profile specifik ruangan. Nama pengguna dan avatar mungkin berbeda dari versi globalnya. + + + + Open the global profile for this user. + Buka profil global untuk pengguna ini. + + + + + Verify + Verifikasi + + + + Start a private chat. + Mulai chat privat. + + + + Kick the user. + Keluarkan pengguna ini. + + + + Ban the user. + Cekal pengguna ini. + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + + Unverify + Hilangkan verifikasi + + + + Sign out device %1 + + + + + You signed out this device. + + + + + Select an avatar + Pilih sebuah avatar + + + + All Files (*) + Semua File (*) + + + + The selected file is not an image + File yang dipilih bukan sebuah gambar + + + + Error while reading file: %1 + Terjadi kesalahan saat membaca file: %1 + + + + UserSettings + + + + Default + Default + + + + UserSettingsPage + + + Minimize to tray + Perkecil ke baki + + + + Start in tray + Mulai di baki + + + + Group's sidebar + Bilah samping grup + + + + Circular Avatars + Avatar Bundar + + + + profile: %1 + profil: %1 + + + + Default + Default + + + + CALLS + PANGGILAN + + + + Cross Signing Keys + Kunci Tanda Tangan Silang + + + + REQUEST + MINTA + + + + DOWNLOAD + UNDUH + + + + Keep the application running in the background after closing the client window. + Membiarkan aplikasi berjalan di latar belakang setelah menutup jendela klien. + + + + Start the application in the background without showing the client window. + Mulai aplikasinya di latar belakang tanpa menunjukkan jendela kliennya. + + + + Change the appearance of user avatars in chats. +OFF - square, ON - Circle. + Ubah penampilan avatar pengguna di chat. +MATI - persegi, NYALA - Lingkaran. + + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + + + + Show a column containing groups and tags next to the room list. + Menampilkan kolom yang berisi grup dan tanda di sebelah daftar ruangan. + + + + Decrypt messages in sidebar + Dekripsikan pesan di bilah samping + + + + Decrypt the messages shown in the sidebar. +Only affects messages in encrypted chats. + Dekripsi pesan yang ditampilkan di bilah samping. +Hanya mempengaruhi pesan di chat terenkripsi. + + + + Privacy Screen + Layar Privasi + + + + When the window loses focus, the timeline will +be blurred. + Ketika jendela kehilangan fokus, linimasanya +akan diburamkan. + + + + Privacy screen timeout (in seconds [0 - 3600]) + Waktu kehabisan layar privasi (dalam detik [0 - 3600]) + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + Tetapkan waktu kehabisan (dalam detik) untuk berapa lama setelah jendela kehilangan +fokus sebelum layarnya akan diburamkan. +Tetapkan ke 0 untuk memburamkan secara langsung setelah kehilangan fokus. Nilai maksimum adalah 1 jam (3600 detik) + + + + Show buttons in timeline + Tampilkan tombol di linimasa + + + + Show buttons to quickly reply, react or access additional options next to each message. + Tampilkan tombol untuk membalas, merekasi, atau mengakses opsi tambahan di sebelah pesan dengan cepat. + + + + Limit width of timeline + Batasi lebar linimasa + + + + Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised + Tetapkan lebar maksimum pesan di linimasa (dalam pixel). Ini bisa membantu keterbacaan di layar lebar, ketika Nheko dimaksimalkan. + + + + Typing notifications + Notifikasi mengetik + + + + Show who is typing in a room. +This will also enable or disable sending typing notifications to others. + Tampilkan siapa yang sedang mengetik dalam ruangan. +Ini akan mengaktifkan atau menonaktifkan pengiriman notifikasi pengetikan ke yang lain. + + + + Sort rooms by unreads + Urutkan ruangan bedasarkan yang belum dibaca + + + + Display rooms with new messages first. +If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room. +If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. + Menampilkan ruangan dengan pesan baru pertama. +Jika ini dimatikan, daftar ruangan hanya diurutkan dari waktu pesan terakhir di ruangan. +Jika ini dinyalakan, ruangan yang mempunyai notifikasi aktif (lingkaran kecil dengan nomor didalam) akan diurutkan di atas. Ruangan, yang dibisukan, masih diurutkan dari waktu, karena Anda tidak pertimangkan mereka sebagai penting dengan ruangan yang lain. + + + + Read receipts + Laporan dibaca + + + + Show if your message was read. +Status is displayed next to timestamps. + Menampilkan jika pesan Anda telah dibaca. +Status akan ditampilkan disebelah waktu menerima pesan. + + + + Send messages as Markdown + Kirim pesan sebagai Markdown + + + + Allow using markdown in messages. +When disabled, all messages are sent as a plain text. + Memperbolehkan menggunakan Markdown di pesan. +Ketika dinonaktifkan, semua pesan akan dikirim sebagai teks biasa. + + + + Play animated images only on hover + Mainkan gambar beranimasi hanya saat kursor diarahkan ke gambar + + + + Desktop notifications + Notifikasi desktop + + + + Notify about received message when the client is not currently focused. + Memberitahukan tentang pesan yang diterima ketika kliennya tidak difokuskan. + + + + Alert on notification + Beritahu saat ada notifikasi + + + + Show an alert when a message is received. +This usually causes the application icon in the task bar to animate in some fashion. + Menampilkan pemberitahuan saat sebuah pesan diterima. +Ini biasanya menyebabkan ikon aplikasi di bilah tugas untuk beranimasi. + + + + Highlight message on hover + Highlight pesan saat kursor di atas pesan + + + + Change the background color of messages when you hover over them. + Mengubah warna background pesan ketika kursor Anda di atas pesannya. + + + + Large Emoji in timeline + Emoji besar di linimasa + + + + Make font size larger if messages with only a few emojis are displayed. + Membuat ukuran font lebih besar jika pesan dengan beberapa emoji ditampilkan. + + + + Send encrypted messages to verified users only + Kirim pesan terenkripsi ke pengguna yang telah diverifikasi saja + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Memerlukan pengguna diverifikasi untuk mengirim pesan terenkripsi ke pengguna. Ini meningkatkan keamanan tetapi membuat enkripsi ujung-ke-ujung lebih susah. + + + + Share keys with verified users and devices + Bagikan kunci dengan pengguna dan perangkat yang telah diverifikasi + + + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Secara otomatis membalas ke permintaan kunci dari pengguna lain, jika mereka terverifikasi, bahkan jika perangkat itu seharusnya tidak mempunyai akses ke kunci itu secara lain. + + + + Online Key Backup + Cadangan Kunci Online + + + + Download message encryption keys from and upload to the encrypted online key backup. + Unduh kunci enkripsi pesan dari dan unggah ke cadangan kunci online terenkripsi. + + + + Enable online key backup + Aktifkan cadangan kunci online + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + Pengembang Nheko merekomendasikan untuk tidak mengaktifkan pencadangan kunci online hingga pencadangan kunci online simetris tersedia. Tetap mengaktifkan? + + + + CACHED + DICACHE + + + + NOT CACHED + TIDAK DICACHE + + + + Scale factor + Faktor skala + + + + Change the scale factor of the whole user interface. + Mengubah faktor skala antarmuka pengguna. + + + + Font size + Ukuran font + + + + Font Family + Keluarga Font + + + + Theme + Tema + + + + Ringtone + Nada Dering + + + + Set the notification sound to play when a call invite arrives + Tetapkan suara notifikasi untuk dimainkan ketika ada panggilan + + + + Microphone + Mikrofon + + + + Camera + Kamera + + + + Camera resolution + Resolusi kamera + + + + Camera frame rate + Frame rate kamera + + + + Allow fallback call assist server + Izinkan panggilan menggunakan bantuan server sebagai cadangan + + + + Will use turn.matrix.org as assist when your home server does not offer one. + Akan menggunakan turn.matrix.org sebagai bantuan jika homeserver Anda tidak menawarkannya. + + + + Device ID + ID Perangkat + + + + Device Fingerprint + Sidik Jari Perangkat + + + + Session Keys + Kunci Perangkat + + + + IMPORT + IMPOR + + + + EXPORT + EKSPOR + + + + ENCRYPTION + ENKRIPSI + + + + GENERAL + UMUM + + + + INTERFACE + ANTARMUKA + + + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Memainkan media seperti GIF atau WEBP ketika kursor di atas medianya. + + + + Touchscreen mode + Mode layar sentuh + + + + Will prevent text selection in the timeline to make touch scrolling easier. + Akan mencegah seleksi teks di linimasi untuk membuat guliran mudah. + + + + Emoji Font Family + Keluarga Font Emoji + + + + Master signing key + Kunci penandatanganan utama + + + + Your most important key. You don't need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys. + Kunci Anda yang paling penting. Anda tidak perlu menyimpannya dalam cache, karena tidak menyimpannya akan memperkecil kemungkinannya untuk dicuri dan hanya diperlukan untuk memutar kunci penandatanganan Anda yang lain. + + + + User signing key + Kunci penandatanganan pengguna + + + + The key to verify other users. If it is cached, verifying a user will verify all their devices. + Kunci untuk memverifikasi pengguna lain. Jika dicache, memverifikasi sebuah pengguna akan memverifikasi semua perangkat mereka. + + + + Self signing key + Kunci penandatanganan diri + + + + The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you. + Kunci untuk memverifikasi perangkat Anda. Jika dicache, memverifikasi salah satu perangkat Anda akan menandainya sebagai terverifikasi untuk semua perangkat Anda yang lain dan untuk pengguna yang telah memverifikasi Anda. + + + + Backup key + Kunci cadangan + + + + The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server. + Kunci untuk mendekripsikan cadangan kunci online. Jika dicache, Anda dapat mengaktifkan kunci cadangan online untuk menyimpan kunci enkripsi yang dienkripsi secara aman di servernya. + + + + Select a file + Pilih sebuah file + + + + All Files (*) + Semua File (*) + + + + Open Sessions File + Buka File Sesi + + + + + + + + + Error + Kesalahan + + + + + File Password + Kata Sandi File + + + + Enter the passphrase to decrypt the file: + Masukkan kata sandi untuk mendekripsi filenya: + + + + + The password cannot be empty + Kata sandi tidak boleh kosong + + + + Enter passphrase to encrypt your session keys: + Masukkan frasa sandi untuk mengenkripsikan kunci sesi Anda: + + + + File to save the exported session keys + File untuk menyimpan kunci sesi yang telah diekspor + + + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Tidak ada chat privat terenkripsi ditemukan dengan pengguna ini. Buat chat privat terenkripsi dengan pengguna ini dan coba lagi. + + + + Waiting + + + Waiting for other partyโ€ฆ + Menunggu untuk mengguna lainโ€ฆ + + + + Waiting for other side to accept the verification request. + Menunggu untuk pengguna yang lain untuk menerima permintaan verifikasi. + + + + Waiting for other side to continue the verification process. + Menunggu untuk pengguna lain untuk melanjutkan proses verifikasi. + + + + Waiting for other side to complete the verification process. + Menunggu untuk pengguna lain untuk menyelesaikan proses verifikasi. + + + + Cancel + Batalkan + + + + WelcomePage + + + Welcome to nheko! The desktop client for the Matrix protocol. + Selamat datang di nheko! Sebuah klien desktop untuk protokol Matrix. + + + + Enjoy your stay! + Nikmati masa tinggal Anda di sini! + + + + REGISTER + DAFTAR + + + + LOGIN + MASUK + + + + descriptiveTime + + + Yesterday + Kemarin + + + + dialogs::CreateRoom + + + Create room + Buat ruangan + + + + Cancel + Batalkan + + + + Name + Nama + + + + Topic + Topik + + + + Alias + Alias + + + + Room Visibility + Visibilitas Ruangan + + + + Room Preset + Preset Ruangan + + + + Direct Chat + Chat Langsung + + + + dialogs::FallbackAuth + + + Open Fallback in Browser + Buka Fallback di Peramban + + + + Cancel + Batalkan + + + + Confirm + Konfirmasi + + + + Open the fallback, follow the steps and confirm after completing them. + Buka fallbacknya, ikuti petunjuknya dan konfirmasi setelah menyelesaikannya. + + + + dialogs::Logout + + + Cancel + Batalkan + + + + Logout. Are you sure? + Keluar. Apakah Anda yakin? + + + + dialogs::PreviewUploadOverlay + + + Upload + Unggah + + + + Cancel + Batalkan + + + + Media type: %1 +Media size: %2 + + Tipe media: %1 +Ukuran media: %2 + + + + + dialogs::ReCaptcha + + + Cancel + Batalkan + + + + Confirm + Konfirmasi + + + + Solve the reCAPTCHA and press the confirm button + Selesaikan reCAPTCHAnya dan tekan tombol konfirmasi + + + + message-description sent: + + + You sent an audio clip + Anda mengirim klip audio + + + + %1 sent an audio clip + %1 mengirim klip audio + + + + You sent an image + Anda mengirim sebuah pesan + + + + %1 sent an image + %1 mengirim sebuah gambar + + + + You sent a file + Anda mengirim sebuah file + + + + %1 sent a file + %1 mengirim sebuah file + + + + You sent a video + Anda mengirim sebuah video + + + + %1 sent a video + %1 mengirim sebuah video + + + + You sent a sticker + Anda mengirim sebuah stiker + + + + %1 sent a sticker + %1 mengirim sebuah stiker + + + + You sent a notification + Anda mengirim sebuah notifikasi + + + + %1 sent a notification + %1 mengirim sebuah notifikasi + + + + You: %1 + Anda: %1 + + + + %1: %2 + %1: %2 + + + + You sent an encrypted message + Anda mengirim sebuah pesan terenkripsi + + + + %1 sent an encrypted message + %1 mengirim sebuah pesan terenkripsi + + + + You placed a call + Anda melakukan panggilan + + + + %1 placed a call + %1 melakukan panggilan + + + + You answered a call + Anda menjawab panggilan + + + + %1 answered a call + %1 menjawab panggilan + + + + You ended a call + Anda mengakhiri panggilan + + + + %1 ended a call + %1 mengakhiri panggilan + + + + utils + + + Unknown Message Type + Tipe Pesan Tidak Dikenal + + + diff --git a/resources/langs/nheko_it.ts b/resources/langs/nheko_it.ts index 5a1d45f8..17e466c8 100644 --- a/resources/langs/nheko_it.ts +++ b/resources/langs/nheko_it.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Chiamata in corso... @@ -56,7 +56,7 @@ CallInvite - + Video Call Chiamata video @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Chiamata Video @@ -117,7 +117,7 @@ CallManager - + Entire screen Schermo completo @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 Impossibile invitare l'utente: %1 - + Invited user: %1 Invitato utente: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. Migrazione della cache alla versione corrente fallita. Questo puรฒ avere diverse cause. Per favore apri una issue e nel frattempo prova ad usare una versione piรน vecchia. In alternativa puoi provare a cancellare la cache manualmente. - + Confirm join Conferma collegamento @@ -151,23 +151,23 @@ Vuoi davvero collegarti a %1? - + Room %1 created. Stanza %1 creata. - + Confirm invite Conferma Invito - + Do you really want to invite %1 (%2)? Vuoi davvero inviare %1 (%2)? - + Failed to invite %1 to %2: %3 Impossibile invitare %1 a %2: %3 @@ -182,7 +182,7 @@ Vuoi davvero allontanare %1 (%2)? - + Kicked user: %1 Scacciato utente: %1 @@ -197,7 +197,7 @@ Vuoi veramente bannare %1 (%2)? - + Failed to ban %1 in %2: %3 Impossibile bannare %1 in %2: %3 @@ -217,7 +217,7 @@ Vuoi veramente reintegrare %1 (%2)? - + Failed to unban %1 in %2: %3 Impossibile rimuovere il ban di %1 in %2: %3 @@ -227,12 +227,12 @@ Rimosso il ban dall'utente: %1 - + Do you really want to start a private chat with %1? Sei sicuro di voler avviare una chat privata con %1? - + Cache migration failed! Migrazione della cache fallita! @@ -247,33 +247,35 @@ La cache sul tuo disco รจ piรน nuova di quella supportata da questa versione di Nheko. Per favore aggiorna o pulisci la tua cache. - + Failed to restore OLM account. Please login again. Impossibile ripristinare l'account OLM. Per favore accedi nuovamente. + + Failed to restore save data. Please login again. Impossibile ripristinare i dati salvati. Per favore accedi nuovamente. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Impossibile configurare le chiavi crittografiche. Risposta del server: %1 %2. Per favore riprova in seguito. - - + + Please try to login again: %1 Per favore prova ad accedere nuovamente: %1 - + Failed to join room: %1 Impossibile accedere alla stanza: %1 - + You joined the room Sei entrato nella stanza @@ -283,7 +285,7 @@ Impossibile rimuovere l'invito: %1 - + Room creation failed: %1 Creazione della stanza fallita: %1 @@ -293,7 +295,7 @@ Impossibile lasciare la stanza: %1 - + Failed to kick %1 from %2: %3 Fallita l'espulsione di %1 da %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets Decifra i segreti @@ -362,12 +364,12 @@ Inserisci la chiave di recupero o la parola chiave per decriptare i tuoi segreti: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: Inserisci la tua chiave di recupero o la parola chiave chiamata %1 per decifrare i tuoi segreti: - + Decryption failed Decrittazione fallita @@ -431,7 +433,7 @@ Cerca - + People Membri @@ -494,6 +496,49 @@ Corrispondono! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - Criptato da un dispositivo non verificato - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Evento Criptato (Nessuna chiave privata per la decriptazione) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - -- Evento Criptato (Chiave non valida per questo indice) -- - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- Errore di Decrittazione (impossibile recuperare le chiavi megolm dal DB) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Errore di Decrittazione (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Evento Criptato (Tipo di evento ignoto) -- - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. Tempo di verifica del dispositivo scaduto. - + Other party canceled the verification. L'altra parte ha annullato la verifica. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close Chiudi @@ -604,6 +613,81 @@ Inoltra Messaggio + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + Annulla + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + Modifica + + + Close Chiudi @@ -645,7 +744,7 @@ InputBar - + Select a file Seleziona un file @@ -655,7 +754,7 @@ Tutti i File (*) - + Failed to upload media. Please try again. Impossibile inviare il file multimediale. Per favore riprova. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ Annulla + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + ID della stanza o alias + + + + LeaveRoomDialog + + + Leave room + Lascia la stanza + + + + Are you sure you want to leave? + Sei sicuro di voler uscire? + + LoginPage @@ -760,25 +885,25 @@ Esempio: https://server.mio:8787 ACCEDI - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org Hai inserito un ID Matrix non valido, es @joe:matrix.org - + Autodiscovery failed. Received malformed response. Ricerca automatica fallita. Ricevuta risposta malformata. - + Autodiscovery failed. Unknown error when requesting .well-known. Ricerca automatica fallita. Errore ignoto durante la richiesta di .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Gli endpoint richiesti non sono stati trovati. Forse non รจ un server Matrix. @@ -788,30 +913,48 @@ Esempio: https://server.mio:8787 Ricevuta risposta malformata. Assicurati che il dominio dell'homeserver sia valido. - + An unknown error occured. Make sure the homeserver domain is valid. Avvenuto un errore sconosciuto. Assicurati che il dominio dell'homeserver sia valido. - + SSO LOGIN ACCESSO SSO - + Empty password Password vuota - + SSO login failed Accesso SSO fallito + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed rimosso @@ -822,7 +965,7 @@ Esempio: https://server.mio:8787 Crittografia abilitata - + room name changed to: %1 nome della stanza cambiato in: %1 @@ -881,6 +1024,11 @@ Esempio: https://server.mio:8787 Negotiating call... + + + Allow them in + + MessageInput @@ -905,7 +1053,7 @@ Esempio: https://server.mio:8787 Scrivi un messaggioโ€ฆ - + Stickers @@ -928,7 +1076,7 @@ Esempio: https://server.mio:8787 MessageView - + Edit Modifica @@ -948,17 +1096,19 @@ Esempio: https://server.mio:8787 Opzioni - + + &Copy - + + Copy &link location - + Re&act @@ -1017,6 +1167,11 @@ Esempio: https://server.mio:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1031,7 +1186,12 @@ Esempio: https://server.mio:8787 Richiesta di verifica ricevuta - + + 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.) + + + + 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? Per permettere agli altri utenti di vedere che dispositivi ti appartengono, puoi verificarli. Questo inoltre permette alle chiavi di recupero di funzionare automaticamente. Verificare %1 adesso? @@ -1077,33 +1237,29 @@ Verificare %1 adesso? Accetta + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 ha inviato un messaggio criptato - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - * %1 %2 - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message Risposta di %1: %2 - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1134,7 +1290,7 @@ Verificare %1 adesso? Nessun microfono trovato. - + Voice @@ -1165,7 +1321,7 @@ Verificare %1 adesso? QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1180,21 +1336,37 @@ Verificare %1 adesso? + + ReadReceipts + + + Read receipts + Ricevute di lettura + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username Nome utente - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. Il nome utente non deve essere vuoto e deve contenere solo i caratteri a-z, 0-9, ., _, =, -, e /. - + Password Password @@ -1214,7 +1386,7 @@ Verificare %1 adesso? Homeserver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. Un server che consente la registrazione. Siccome matrix รจ decentralizzata, devi prima trovare un server su cui registrarti o ospitarne uno tuo. @@ -1224,27 +1396,17 @@ Verificare %1 adesso? REGISTRATI - - No supported registration flows! - Non ci sono processi di registrazione supportati! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. Ricerca automatica fallita. Ricevuta risposta malformata. - + Autodiscovery failed. Unknown error when requesting .well-known. Ricerca automatica fallita. Errore ignoto durante la richiesta di .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Gli endpoint richiesti non sono stati trovati. Forse non รจ un server Matrix. @@ -1259,17 +1421,17 @@ Verificare %1 adesso? Avvenuto un errore sconosciuto. Assicurati che il dominio dell'homeserver sia valido. - + Password is not long enough (min 8 chars) La password non รจ abbastanza lunga (minimo 8 caratteri) - + Passwords don't match Le password non corrispondono - + Invalid server name Nome del server non valido @@ -1277,7 +1439,7 @@ Verificare %1 adesso? ReplyPopup - + Close Chiudi @@ -1287,10 +1449,28 @@ Verificare %1 adesso? + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored nessuna versione memorizzata @@ -1298,7 +1478,7 @@ Verificare %1 adesso? RoomList - + New tag @@ -1307,16 +1487,6 @@ Verificare %1 adesso? Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1348,7 +1518,7 @@ Verificare %1 adesso? - + Status Message @@ -1368,12 +1538,35 @@ Verificare %1 adesso? - + Logout Disconnettiti - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Chiudi + + + Start a new chat Inizia una nuova discussione @@ -1393,7 +1586,7 @@ Verificare %1 adesso? Elenco delle stanze - + User settings Impostazioni utente @@ -1401,12 +1594,12 @@ Verificare %1 adesso? RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1419,16 +1612,36 @@ Verificare %1 adesso? Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1458,7 +1671,12 @@ Verificare %1 adesso? - + + Room access + + + + Anyone and guests @@ -1473,7 +1691,17 @@ Verificare %1 adesso? - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1519,12 +1747,12 @@ Verificare %1 adesso? - + Failed to enable encryption: %1 Impossibile abilitare la crittografia: %1 - + Select an avatar Scegli un avatar @@ -1544,8 +1772,8 @@ Verificare %1 adesso? Errore durante la lettura del file: %1 - - + + Failed to upload image: %s Impossibile fare l'upload dell'immagine: %s @@ -1553,21 +1781,49 @@ Verificare %1 adesso? RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1622,6 +1878,121 @@ Verificare %1 adesso? Annulla + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1674,18 +2045,18 @@ Verificare %1 adesso? TimelineModel - + Message redaction failed: %1 Oscuramento del messaggio fallito: %1 - + Failed to encrypt event, sending aborted! - + Save image Salva immagine @@ -1705,7 +2076,7 @@ Verificare %1 adesso? Salva file - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1714,7 +2085,7 @@ Verificare %1 adesso? - + %1 opened the room to the public. %1 ha aperto la stanza al pubblico. @@ -1724,7 +2095,17 @@ Verificare %1 adesso? %1 ha configurato questa stanza per richiedere un invito per entrare. - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. %1 ha configurato questa stanza affinchรฉ sia aperta ai visitatori. @@ -1744,12 +2125,12 @@ Verificare %1 adesso? %1 ha reso la cronologia della stanza visibile ai membri da questo momento in poi. - + %1 set the room history visible to members since they were invited. %1 ha reso la cronologia della stanza visibile ai membri dal momento in cui sono stati invitati. - + %1 set the room history visible to members since they joined the room. %1 ha reso la cronologia della stanza visibile ai membri dal momento in cui sono entrati nella stanza. @@ -1759,12 +2140,12 @@ Verificare %1 adesso? %1 ha cambiato i permessi della stanza. - + %1 was invited. %1 รจ stato invitato. - + %1 changed their avatar. %1 ha cambiato il suo avatar. @@ -1774,12 +2155,17 @@ Verificare %1 adesso? - + %1 joined. %1 รจ entrato. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. %1 ha rifiutato il suo invito. @@ -1809,32 +2195,32 @@ Verificare %1 adesso? %1 รจ stato bannato. - + Reason: %1 - + %1 redacted their knock. %1 ha oscurato la sua bussata. - + You joined this room. Sei entrato in questa stanza. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. Rifiutata la bussata di %1. @@ -1853,7 +2239,7 @@ Verificare %1 adesso? TimelineRow - + Edited @@ -1861,12 +2247,17 @@ Verificare %1 adesso? TimelineView - + No room open Nessuna stanza aperta - + + No preview available + + + + %1 member(s) @@ -1891,28 +2282,40 @@ Verificare %1 adesso? - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options Opzioni della stanza @@ -1950,10 +2353,35 @@ Verificare %1 adesso? Esci + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1963,33 +2391,98 @@ Verificare %1 adesso? - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar Scegli un avatar @@ -2012,8 +2505,8 @@ Verificare %1 adesso? UserSettings - - + + Default @@ -2021,7 +2514,7 @@ Verificare %1 adesso? UserSettingsPage - + Minimize to tray Minimizza nella tray @@ -2031,22 +2524,22 @@ Verificare %1 adesso? Avvia nella tray - + Group's sidebar Barra laterale dei gruppi - + Circular Avatars Avatar Circolari - + profile: %1 - + Default @@ -2071,7 +2564,7 @@ Verificare %1 adesso? - + Keep the application running in the background after closing the client window. @@ -2086,6 +2579,16 @@ Verificare %1 adesso? OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2114,7 +2617,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2169,7 +2672,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts Ricevute di lettura @@ -2180,7 +2683,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown Invia messaggi come Markdown @@ -2192,6 +2695,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications Notifiche desktop @@ -2232,12 +2740,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2247,7 +2790,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor Fattore di scala @@ -2322,7 +2865,7 @@ This usually causes the application icon in the task bar to animate in some fash Impronta digitale del dispositivo - + Session Keys Chiavi di Sessione @@ -2342,17 +2885,22 @@ This usually causes the application icon in the task bar to animate in some fash CRITTOGRAFIA - + GENERAL GENERALE - + INTERFACE INTERFACCIA - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2367,12 +2915,7 @@ This usually causes the application icon in the task bar to animate in some fash Famiglia dei caratteri delle Emoji - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2392,7 +2935,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2422,14 +2965,14 @@ This usually causes the application icon in the task bar to animate in some fash Tutti i File (*) - + Open Sessions File Apri File delle Sessioni - + @@ -2437,19 +2980,19 @@ This usually causes the application icon in the task bar to animate in some fash Errore - - + + File Password Password del File - + Enter the passphrase to decrypt the file: Inserisci la passphrase per decriptare il file: - + The password cannot be empty La password non puรฒ essere vuota @@ -2464,6 +3007,14 @@ This usually causes the application icon in the task bar to animate in some fash File ove salvare le chiavi di sessione esportate + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2589,37 +3140,6 @@ This usually causes the application icon in the task bar to animate in some fash Apri il ripiego, segui i passaggi e conferma dopo averli completati. - - dialogs::JoinRoom - - - Join - Entra - - - - Cancel - Annulla - - - - Room ID or alias - ID della stanza o alias - - - - dialogs::LeaveRoom - - - Cancel - Annulla - - - - Are you sure you want to leave? - Sei sicuro di voler uscire? - - dialogs::Logout @@ -2673,32 +3193,6 @@ Peso media: %2 Risolvi il reCAPTCHA e premi il pulsante di conferma - - dialogs::ReadReceipts - - - Read receipts - Ricevute di lettura - - - - Close - Chiudi - - - - dialogs::ReceiptItem - - - Today %1 - Oggi %1 - - - - Yesterday %1 - Ieri %1 - - message-description sent: @@ -2712,47 +3206,47 @@ Peso media: %2 %1 ha inviato una clip audio - + You sent an image Hai inviato un'immagine - + %1 sent an image %1 ha inviato un'immagine - + You sent a file Hai inviato un file - + %1 sent a file %1 ha inviato un file - + You sent a video Hai inviato un video - + %1 sent a video %1 ha inviato un video - + You sent a sticker Hai inviato uno sticker - + %1 sent a sticker %1 ha inviato uno sticker - + You sent a notification Hai inviato una notifica @@ -2767,7 +3261,7 @@ Peso media: %2 Tu: %1 - + %1: %2 %1: %2 @@ -2787,27 +3281,27 @@ Peso media: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2815,7 +3309,7 @@ Peso media: %2 utils - + Unknown Message Type Tipo di Messaggio sconosciuto diff --git a/resources/langs/nheko_ja.ts b/resources/langs/nheko_ja.ts index 1ec862b0..7ae33c58 100644 --- a/resources/langs/nheko_ja.ts +++ b/resources/langs/nheko_ja.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... @@ -56,7 +56,7 @@ CallInvite - + Video Call @@ -74,7 +74,7 @@ CallInviteBar - + Video Call @@ -117,7 +117,7 @@ CallManager - + Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 ใƒฆใƒผใ‚ถใƒผใ‚’ๆ‹›ๅพ…ใงใใพใ›ใ‚“ใงใ—ใŸ: %1 - + Invited user: %1 ๆ‹›ๅพ…ใ•ใ‚ŒใŸใƒฆใƒผใ‚ถใƒผ: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. - + Confirm invite - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 %2ใซ%1ใ‚’ๆ‹›ๅพ…ใงใใพใ›ใ‚“ใงใ—ใŸ: %3 @@ -182,7 +182,7 @@ - + Kicked user: %1 ไธ€ๆ™‚็š„ใซ่ฟฝๆ”พใ•ใ‚ŒใŸใƒฆใƒผใ‚ถใƒผ: %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 %2ใง%1ใ‚’ๆฐธไน…่ฟฝๆ”พใงใใพใ›ใ‚“ใงใ—ใŸ: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 %2ใง%1ใฎๆฐธไน…่ฟฝๆ”พใ‚’่งฃ้™คใงใใพใ›ใ‚“ใงใ—ใŸ: %3 @@ -227,12 +227,12 @@ ๆฐธไน…่ฟฝๆ”พใ‚’่งฃ้™คใ•ใ‚ŒใŸใƒฆใƒผใ‚ถใƒผ: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -247,33 +247,35 @@ - + Failed to restore OLM account. Please login again. OLMใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’ๅพฉๅ…ƒใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใƒญใ‚ฐใ‚คใƒณใ—ใฆไธ‹ใ•ใ„ใ€‚ + + Failed to restore save data. Please login again. ใ‚ปใƒผใƒ–ใƒ‡ใƒผใ‚ฟใ‚’ๅพฉๅ…ƒใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใƒญใ‚ฐใ‚คใƒณใ—ใฆไธ‹ใ•ใ„ใ€‚ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. ๆš—ๅทๅŒ–้ตใ‚’่จญๅฎšใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ใ‚ตใƒผใƒใƒผใฎๅฟœ็ญ”: %1 %2. ๅพŒใงใ‚„ใ‚Š็›ดใ—ใฆไธ‹ใ•ใ„ใ€‚ - - + + Please try to login again: %1 ใ‚‚ใ†ไธ€ๅบฆใƒญใ‚ฐใ‚คใƒณใ—ใฆใฟใฆไธ‹ใ•ใ„: %1 - + Failed to join room: %1 ้ƒจๅฑ‹ใซๅ‚ๅŠ ใงใใพใ›ใ‚“ใงใ—ใŸ: %1 - + You joined the room ้ƒจๅฑ‹ใซๅ‚ๅŠ ใ—ใพใ—ใŸ @@ -283,7 +285,7 @@ ๆ‹›ๅพ…ใ‚’ๅ‰Š้™คใงใใพใ›ใ‚“ใงใ—ใŸ: %1 - + Room creation failed: %1 ้ƒจๅฑ‹ใ‚’ไฝœๆˆใงใใพใ›ใ‚“ใงใ—ใŸ: %1 @@ -293,7 +295,7 @@ ้ƒจๅฑ‹ใ‹ใ‚‰ๅ‡บใ‚‰ใ‚Œใพใ›ใ‚“ใงใ—ใŸ: %1 - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- ๆš—ๅทๅŒ–ใ‚คใƒ™ใƒณใƒˆ (ๅพฉๅท้ตใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- ๅพฉๅทใ‚จใƒฉใƒผ (ใƒ‡ใƒผใ‚ฟใƒ™ใƒผใ‚นใ‹ใ‚‰megolm้ตใ‚’ๅ–ๅพ—ใงใใพใ›ใ‚“ใงใ—ใŸ) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- ๅพฉๅทใ‚จใƒฉใƒผ (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- ๆš—ๅทๅŒ–ใ‚คใƒ™ใƒณใƒˆ (ไธๆ˜Žใชใ‚คใƒ™ใƒณใƒˆๅž‹ใงใ™) -- - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close ้–‰ใ˜ใ‚‹ @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + ใ‚ญใƒฃใƒณใ‚ปใƒซ + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close ้–‰ใ˜ใ‚‹ @@ -645,7 +744,7 @@ InputBar - + Select a file ใƒ•ใ‚กใ‚คใƒซใ‚’้ธๆŠž @@ -655,7 +754,7 @@ ๅ…จใฆใฎใƒ•ใ‚กใ‚คใƒซ (*) - + Failed to upload media. Please try again. ใƒกใƒ‡ใ‚ฃใ‚ขใ‚’ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ใ‚„ใ‚Š็›ดใ—ใฆไธ‹ใ•ใ„ใ€‚ @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ ใ‚ญใƒฃใƒณใ‚ปใƒซ + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + ้ƒจๅฑ‹ใฎIDๅˆใฏๅˆฅๅ + + + + LeaveRoomDialog + + + Leave room + ้ƒจๅฑ‹ใ‚’ๅ‡บใ‚‹ + + + + Are you sure you want to leave? + ๆœฌๅฝ“ใซ้€€ๅ‡บใ—ใพใ™ใ‹? + + LoginPage @@ -756,25 +881,25 @@ Example: https://server.my:8787 ใƒญใ‚ฐใ‚คใƒณ - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. ่‡ชๅ‹•ๆคœๅ‡บใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ไธๆญฃใชๅฝขๅผใฎๅฟœ็ญ”ใ‚’ๅ—ไฟกใ—ใพใ—ใŸใ€‚ - + Autodiscovery failed. Unknown error when requesting .well-known. ่‡ชๅ‹•ๆคœๅ‡บใงใใพใ›ใ‚“ใงใ—ใŸใ€‚.well-known่ฆๆฑ‚ๆ™‚ใฎไธๆ˜Žใชใ‚จใƒฉใƒผใ€‚ - + The required endpoints were not found. Possibly not a Matrix server. ๅฟ…่ฆใช็ซฏ็‚นใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚Matrixใ‚ตใƒผใƒใƒผใงใฏใชใ„ใ‹ใ‚‚ใ—ใ‚Œใพใ›ใ‚“ใ€‚ @@ -784,30 +909,48 @@ Example: https://server.my:8787 ไธๆญฃใชๅฝขๅผใฎๅฟœ็ญ”ใ‚’ๅ—ไฟกใ—ใพใ—ใŸใ€‚ใƒ›ใƒผใƒ ใ‚ตใƒผใƒใƒผใฎใƒ‰ใƒกใ‚คใƒณๅใŒๆœ‰ๅŠนใงใ‚ใ‚‹ใ‹ใ‚’็ขบ่ชใ—ใฆไธ‹ใ•ใ„ใ€‚ - + An unknown error occured. Make sure the homeserver domain is valid. ไธๆ˜Žใชใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸใ€‚ใƒ›ใƒผใƒ ใ‚ตใƒผใƒใƒผใฎใƒ‰ใƒกใ‚คใƒณๅใŒๆœ‰ๅŠนใงใ‚ใ‚‹ใ‹ใ‚’็ขบ่ชใ—ใฆไธ‹ใ•ใ„ใ€‚ - + SSO LOGIN - + Empty password ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใŒๅ…ฅๅŠ›ใ•ใ‚Œใฆใ„ใพใ›ใ‚“ - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed @@ -818,7 +961,7 @@ Example: https://server.my:8787 ๆš—ๅทๅŒ–ใŒๆœ‰ๅŠนใงใ™ - + room name changed to: %1 ้ƒจๅฑ‹ๅใŒๅค‰ๆ›ดใ•ใ‚Œใพใ—ใŸ: %1 @@ -877,6 +1020,11 @@ Example: https://server.my:8787 Negotiating call... + + + Allow them in + + MessageInput @@ -901,7 +1049,7 @@ Example: https://server.my:8787 ใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’ๆ›ธใ... - + Stickers @@ -924,7 +1072,7 @@ Example: https://server.my:8787 MessageView - + Edit @@ -944,17 +1092,19 @@ Example: https://server.my:8787 ใ‚ชใƒ—ใ‚ทใƒงใƒณ - + + &Copy - + + Copy &link location - + Re&act @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1072,33 +1232,29 @@ Example: https://server.my:8787 ๅฎน่ช + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1ใŒๆš—ๅทๅŒ–ใ•ใ‚ŒใŸใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’้€ไฟกใ—ใพใ—ใŸ - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + ้–‹ๅฐ็ขบ่ช + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username ใƒฆใƒผใ‚ถใƒผๅ - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password ใƒ‘ใ‚นใƒฏใƒผใƒ‰ @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 ็™ป้Œฒ - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. ่‡ชๅ‹•ๆคœๅ‡บใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ไธๆญฃใชๅฝขๅผใฎๅฟœ็ญ”ใ‚’ๅ—ไฟกใ—ใพใ—ใŸใ€‚ - + Autodiscovery failed. Unknown error when requesting .well-known. ่‡ชๅ‹•ๆคœๅ‡บใงใใพใ›ใ‚“ใงใ—ใŸใ€‚.well-known่ฆๆฑ‚ๆ™‚ใฎไธๆ˜Žใชใ‚จใƒฉใƒผใ€‚ - + The required endpoints were not found. Possibly not a Matrix server. ๅฟ…่ฆใช็ซฏ็‚นใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚Matrixใ‚ตใƒผใƒใƒผใงใฏใชใ„ใ‹ใ‚‚ใ—ใ‚Œใพใ›ใ‚“ใ€‚ @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 ไธๆ˜Žใชใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸใ€‚ใƒ›ใƒผใƒ ใ‚ตใƒผใƒใƒผใฎใƒ‰ใƒกใ‚คใƒณๅใŒๆœ‰ๅŠนใงใ‚ใ‚‹ใ‹ใ‚’็ขบ่ชใ—ใฆไธ‹ใ•ใ„ใ€‚ - + Password is not long enough (min 8 chars) ใƒ‘ใ‚นใƒฏใƒผใƒ‰้•ทใŒไธ่ถณใ—ใฆใ„ใพใ™ (ๆœ€ๅฐ8ๆ–‡ๅญ—) - + Passwords don't match ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใŒไธ€่‡ดใ—ใพใ›ใ‚“ - + Invalid server name ็„กๅŠนใชใ‚ตใƒผใƒใƒผๅใงใ™ @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close ้–‰ใ˜ใ‚‹ @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored ใƒใƒผใ‚ธใƒงใƒณใŒไฟๅญ˜ใ•ใ‚Œใฆใ„ใพใ›ใ‚“ @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout ใƒญใ‚ฐใ‚ขใ‚ฆใƒˆ - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + ้–‰ใ˜ใ‚‹ + + + Start a new chat ๆ–ฐใ—ใ„ใƒใƒฃใƒƒใƒˆใ‚’้–‹ๅง‹ @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 ้ƒจๅฑ‹ไธ€่ฆง - + User settings ใƒฆใƒผใ‚ถใƒผ่จญๅฎš @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1413,16 +1606,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1452,7 +1665,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1467,7 +1685,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1513,12 +1741,12 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 ๆš—ๅทๅŒ–ใ‚’ๆœ‰ๅŠนใซใงใใพใ›ใ‚“ใงใ—ใŸ: %1 - + Select an avatar ใ‚ขใƒใ‚ฟใƒผใ‚’้ธๆŠž @@ -1538,8 +1766,8 @@ Example: https://server.my:8787 ใƒ•ใ‚กใ‚คใƒซใฎ่ชญใฟ่พผใฟๆ™‚ใซใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸ: %1 - - + + Failed to upload image: %s ็”ปๅƒใ‚’ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใงใใพใ›ใ‚“ใงใ—ใŸ: %s @@ -1547,21 +1775,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1616,6 +1872,121 @@ Example: https://server.my:8787 ใ‚ญใƒฃใƒณใ‚ปใƒซ + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1668,18 +2039,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 ใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’็ทจ้›†ใงใใพใ›ใ‚“ใงใ—ใŸ: %1 - + Failed to encrypt event, sending aborted! - + Save image ็”ปๅƒใ‚’ไฟๅญ˜ @@ -1699,7 +2070,7 @@ Example: https://server.my:8787 ใƒ•ใ‚กใ‚คใƒซใ‚’ไฟๅญ˜ - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1707,7 +2078,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1717,7 +2088,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1737,12 +2118,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1752,12 +2133,12 @@ Example: https://server.my:8787 - + %1 was invited. %1ใŒๆ‹›ๅพ…ใ•ใ‚Œใพใ—ใŸใ€‚ - + %1 changed their avatar. %1ใŒใ‚ขใƒใ‚ฟใƒผใ‚’ๅค‰ๆ›ดใ—ใพใ—ใŸใ€‚ @@ -1767,12 +2148,17 @@ Example: https://server.my:8787 - + %1 joined. %1ใŒๅ‚ๅŠ ใ—ใพใ—ใŸใ€‚ - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. %1ใŒๆ‹›ๅพ…ใ‚’ๆ‹’ๅฆใ—ใพใ—ใŸใ€‚ @@ -1802,32 +2188,32 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. %1ใŒใƒŽใƒƒใ‚ฏใ‚’็ทจ้›†ใ—ใพใ—ใŸใ€‚ - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. %1ใ‹ใ‚‰ใฎใƒŽใƒƒใ‚ฏใ‚’ๆ‹’ๅฆใ—ใพใ—ใŸใ€‚ @@ -1846,7 +2232,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1854,12 +2240,17 @@ Example: https://server.my:8787 TimelineView - + No room open ้ƒจๅฑ‹ใŒ้–‹ใ„ใฆใ„ใพใ›ใ‚“ - + + No preview available + + + + %1 member(s) @@ -1884,28 +2275,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options ้ƒจๅฑ‹ใฎใ‚ชใƒ—ใ‚ทใƒงใƒณ @@ -1943,10 +2346,35 @@ Example: https://server.my:8787 ็ต‚ไบ† + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1956,33 +2384,98 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar ใ‚ขใƒใ‚ฟใƒผใ‚’้ธๆŠž @@ -2005,8 +2498,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2014,7 +2507,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray ใƒˆใƒฌใ‚คใธๆœ€ๅฐๅŒ– @@ -2024,22 +2517,22 @@ Example: https://server.my:8787 ใƒˆใƒฌใ‚คใง่ตทๅ‹• - + Group's sidebar ใ‚ฐใƒซใƒผใƒ—ใ‚ตใ‚คใƒ‰ใƒใƒผ - + Circular Avatars ๅ††ๅฝขใ‚ขใƒใ‚ฟใƒผ - + profile: %1 - + Default @@ -2064,7 +2557,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2079,6 +2572,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2107,7 +2610,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2162,7 +2665,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts ้–‹ๅฐ็ขบ่ช @@ -2173,7 +2676,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown ใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’Markdownใจใ—ใฆ้€ไฟก @@ -2185,6 +2688,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—้€š็Ÿฅ @@ -2225,12 +2733,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2240,7 +2783,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor ๅฐบๅบฆไฟ‚ๆ•ฐ @@ -2315,7 +2858,7 @@ This usually causes the application icon in the task bar to animate in some fash ใƒ‡ใƒใ‚คใ‚นใฎๆŒ‡็ด‹ - + Session Keys ใ‚ปใƒƒใ‚ทใƒงใƒณ้ต @@ -2335,17 +2878,22 @@ This usually causes the application icon in the task bar to animate in some fash ๆš—ๅทๅŒ– - + GENERAL ๅ…จ่ˆฌ - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2360,12 +2908,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2385,7 +2928,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2415,14 +2958,14 @@ This usually causes the application icon in the task bar to animate in some fash ๅ…จใฆใฎใƒ•ใ‚กใ‚คใƒซ (*) - + Open Sessions File ใ‚ปใƒƒใ‚ทใƒงใƒณใƒ•ใ‚กใ‚คใƒซใ‚’้–‹ใ - + @@ -2430,19 +2973,19 @@ This usually causes the application icon in the task bar to animate in some fash ใ‚จใƒฉใƒผ - - + + File Password ใƒ•ใ‚กใ‚คใƒซใฎใƒ‘ใ‚นใƒฏใƒผใƒ‰ - + Enter the passphrase to decrypt the file: ใƒ•ใ‚กใ‚คใƒซใ‚’ๅพฉๅทใ™ใ‚‹ใŸใ‚ใฎใƒ‘ใ‚นใƒ•ใƒฌใƒผใ‚บใ‚’ๅ…ฅๅŠ›ใ—ใฆไธ‹ใ•ใ„: - + The password cannot be empty ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚’็ฉบใซใฏใงใใพใ›ใ‚“ @@ -2457,6 +3000,14 @@ This usually causes the application icon in the task bar to animate in some fash ใ‚จใ‚ฏใ‚นใƒใƒผใƒˆใ•ใ‚ŒใŸใ‚ปใƒƒใ‚ทใƒงใƒณ้ตใ‚’ไฟๅญ˜ใ™ใ‚‹ใƒ•ใ‚กใ‚คใƒซ + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2582,37 +3133,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - ๅ‚ๅŠ  - - - - Cancel - ใ‚ญใƒฃใƒณใ‚ปใƒซ - - - - Room ID or alias - ้ƒจๅฑ‹ใฎIDๅˆใฏๅˆฅๅ - - - - dialogs::LeaveRoom - - - Cancel - ใ‚ญใƒฃใƒณใ‚ปใƒซ - - - - Are you sure you want to leave? - ๆœฌๅฝ“ใซ้€€ๅ‡บใ—ใพใ™ใ‹? - - dialogs::Logout @@ -2666,32 +3186,6 @@ Media size: %2 reCAPTCHAใซ่งฃ็ญ”ใ—ใฆใ€็ขบ่ชใƒœใ‚ฟใƒณใ‚’ๆŠผใ—ใฆไธ‹ใ•ใ„ - - dialogs::ReadReceipts - - - Read receipts - ้–‹ๅฐ็ขบ่ช - - - - Close - ้–‰ใ˜ใ‚‹ - - - - dialogs::ReceiptItem - - - Today %1 - ไปŠๆ—ฅ %1 - - - - Yesterday %1 - ๆ˜จๆ—ฅ %1 - - message-description sent: @@ -2705,47 +3199,47 @@ Media size: %2 %1ใŒ้Ÿณๅฃฐใƒ‡ใƒผใ‚ฟใ‚’้€ไฟกใ—ใพใ—ใŸ - + You sent an image ็”ปๅƒใ‚’้€ไฟกใ—ใพใ—ใŸ - + %1 sent an image %1ใŒ็”ปๅƒใ‚’้€ไฟกใ—ใพใ—ใŸ - + You sent a file ใƒ•ใ‚กใ‚คใƒซใ‚’้€ไฟกใ—ใพใ—ใŸ - + %1 sent a file %1ใŒใƒ•ใ‚กใ‚คใƒซใ‚’้€ไฟกใ—ใพใ—ใŸ - + You sent a video ๅ‹•็”ปใ‚’้€ไฟกใ—ใพใ—ใŸ - + %1 sent a video %1ใŒๅ‹•็”ปใ‚’้€ไฟกใ—ใพใ—ใŸ - + You sent a sticker ใ‚นใƒ†ใƒƒใ‚ซใƒผใ‚’้€ไฟกใ—ใพใ—ใŸ - + %1 sent a sticker %1ใŒใ‚นใƒ†ใƒƒใ‚ซใƒผใ‚’้€ไฟกใ—ใพใ—ใŸ - + You sent a notification ้€š็Ÿฅใ‚’้€ไฟกใ—ใพใ—ใŸ @@ -2760,7 +3254,7 @@ Media size: %2 ใ‚ใชใŸ: %1 - + %1: %2 %1: %2 @@ -2780,27 +3274,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2808,7 +3302,7 @@ Media size: %2 utils - + Unknown Message Type ไธๆ˜Žใชใƒกใƒƒใ‚ปใƒผใ‚ธๅž‹ใงใ™ diff --git a/resources/langs/nheko_ml.ts b/resources/langs/nheko_ml.ts index 011107c2..d8e25ff2 100644 --- a/resources/langs/nheko_ml.ts +++ b/resources/langs/nheko_ml.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... เดตเดฟเดณเดฟเด•เตเด•เตเดจเตเดจเต... @@ -17,7 +17,7 @@ You are screen sharing - + เดจเดฟเด™เตเด™เตพ เดธเตเด•เตเดฐเต€เตป เดชเด™เตเด•เดฟเดŸเตเดจเตเดจเต @@ -56,7 +56,7 @@ CallInvite - + Video Call เดตเต€เดกเดฟเดฏเต‡เดพ เด•เต‡เดพเตพ @@ -74,7 +74,7 @@ CallInviteBar - + Video Call เดตเต€เดกเดฟเดฏเต‡เดพ เด•เต‡เดพเตพ @@ -91,7 +91,7 @@ Accept - + เดธเตเดตเต€เด•เดฐเดฟเด•เตเด•เตเด• @@ -117,64 +117,64 @@ CallManager - + Entire screen - + เดฎเตเดดเตเดตเตป เดธเตเด•เตเดฐเต€เตป ChatPage - + Failed to invite user: %1 เด‰เดชเดฏเต‹เด•เตเดคเดพเดตเดฟเดจเต† เด•เตเดทเดฃเดฟเด•เตเด•เตเดจเตเดจเดคเดฟเตฝ เดชเดฐเดพเดœเดฏเดชเตเดชเต†เดŸเตเดŸเต: %1 - + Invited user: %1 เด•เตเดทเดฃเดฟเดšเตเดš เด‰เดชเดฏเต‹เด•เตเดคเดพเดตเต:% 1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Confirm join - + เดšเต‡เดฐเตเดจเตเดจเดคเต เด‰เดฑเดชเตเดชเดพเด•เตเด•เตเด• Do you really want to join %1? - + เดจเดฟเด™เตเด™เตพเด•เตเด•เต %1 -เตฝ เดšเต‡เดฐเดพเตป เด†เด—เตเดฐเดนเด‚ เด‰เดฃเตเดŸเต‹? - + Room %1 created. %1 เดฎเตเดฑเดฟ เดธเตƒเดทเตเดŸเดฟเดšเตเดšเต - + Confirm invite เด•เตเดทเดฃเด‚ เด‰เดฑเดชเตเดชเดพเด•เตเด•เต - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 Confirm kick - + เดชเตเดฑเดคเตเดคเดพเด•เตเด•เตฝ เด‰เดฑเดชเตเดชเดพเด•เตเด•เตเด• @@ -182,9 +182,9 @@ - + Kicked user: %1 - + เด‰เดชเดฏเต‹เด•เตเดคเดพเดตเดฟเดจเต† เดชเตเดฑเดคเตเดคเดพเด•เตเด•เดฟ: %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -247,33 +247,35 @@ - + Failed to restore OLM account. Please login again. + + Failed to restore save data. Please login again. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - - + + Please try to login again: %1 - + เดฆเดฏเดตเดพเดฏเดฟ เดตเต€เดฃเตเดŸเตเด‚ เดฒเต‹เด—เดฟเตป เดšเต†เดฏเตเดฏเดพเตป เดจเต‹เด•เตเด•เตเด•: %1 - + Failed to join room: %1 เดฎเตเดฑเดฟเดฏเดฟเตฝ เดšเต‡เดฐเตเดจเตเดจเดคเดฟเตฝ เดชเดฐเดพเดœเดฏเด‚: %1 - + You joined the room เดจเดฟเด™เตเด™เตพ เดฎเตเดฑเดฟเดฏเดฟเตฝ เดšเต‡เตผเดจเตเดจเต @@ -283,9 +285,9 @@ - + Room creation failed: %1 - + เดฎเตเดฑเดฟ เดธเตƒเดทเตเดŸเดฟเด•เตเด•เตเดจเตเดจเดคเต เดชเดฐเดพเดœเดฏเดชเตเดชเต†เดŸเตเดŸเต: %1 @@ -293,7 +295,7 @@ - + Failed to kick %1 from %2: %3 @@ -321,7 +323,7 @@ Favourites - + เดชเตเดฐเดฟเดฏเดชเตเดชเต†เดŸเตเดŸเดต @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -382,7 +384,7 @@ Verification Code - + เด‰เดฑเดชเตเดชเดพเด•เตเด•เตฝ เด•เต‹เดกเต @@ -431,7 +433,7 @@ เดคเดฟเดฐเดฏเตเด• - + People เด†เดณเตเด•เตพ @@ -448,17 +450,17 @@ Activity - + เดชเตเดฐเดตเตผเดคเตเดคเดจเด‚ Travel - + เดฏเดพเดคเตเดฐ Objects - + เดธเดพเดงเดจเด™เตเด™เตพ @@ -468,7 +470,7 @@ Flags - + เดชเดคเดพเด•เด•เตพ @@ -476,7 +478,7 @@ Verification Code - + เด‰เดฑเดชเตเดชเดพเด•เตเด•เตฝ เด•เต‹เดกเต @@ -494,6 +496,49 @@ เด…เดต เดชเตŠเดฐเตเดคเตเดคเดชเตเดชเต†เดŸเตเดจเตเดจเต! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + เด•เต€ เด…เดญเตเดฏเตผ + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -567,7 +567,7 @@ Verification failed - + เด‰เดฑเดชเตเดชเดพเด•เตเด•เตฝ เดชเดฐเดพเดœเดฏเดชเตเดชเต†เดŸเตเดŸเต @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close เด…เดŸเดฏเตโ€Œเด•เตเด•เตเด• @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + เดšเดฟเดคเตเดฐเด™เตเด™เตพ เดšเต‡เตผเด•เตเด•เตเด• + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + เดธเตเดฑเตเดฑเดฟเด•เตเด•เดฑเตเด•เตพ(*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + + + + + Packname + เดชเดพเด•เตเด•เดฟเดจเตเดฑเต† เดชเต‡เดฐเต + + + + Attribution + + + + + + Use as Emoji + เด‡เดฎเต‹เดœเดฟ เด†เดฏเดฟ เด‰เดชเดฏเต‹เด—เดฟเด•เตเด•เตเด• + + + + + Use as Sticker + เดธเตเดฑเตเดฑเดฟเด•เตเด•เดฑเดพเดฏเดฟ เด‰เดชเดฏเต‹เด—เดฟเด•เตเด•เตเด• + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + เดจเต€เด•เตเด•เด‚ เดšเต†เดฏเตเดฏเตเด• + + + + Cancel + เดฑเดฆเตเดฆเดพเด•เตเด•เต + + + + Save + เดธเด‚เดฐเด•เตเดทเดฟเด•เตเด•เตเด• + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + เด…เด•เตเด•เต—เดฃเตเดŸเต เดชเดพเด•เตเด•เต เดธเตƒเดทเตเดŸเดฟเด•เตเด•เตเด• + + + + New room pack + เดชเตเดคเดฟเดฏ เดฎเตเดฑเดฟ เดชเดพเด•เตเด•เต + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + เดคเดฟเดฐเตเดคเตเดคเตเด• + + + Close เด…เดŸเดฏเตโ€Œเด•เตเด•เตเด• @@ -645,17 +744,17 @@ InputBar - + Select a file เด’เดฐเต เดซเดฏเตฝ เดคเดฟเดฐเดžเตเดžเต†เดŸเตเด•เตเด•เตเด• All Files (*) - + เดŽเดฒเตเดฒเดพ เดซเดฏเดฒเตเด•เดณเตเด‚ (*) - + Failed to upload media. Please try again. @@ -663,9 +762,9 @@ InviteDialog - + Invite users to %1 - + %1 - เดฒเต‡เด•เตเด•เต เด‰เดชเดฏเต‹เด•เตเดคเดพเด•เตเด•เดณเต† เด•เตเดทเดฃเดฟเด•เตเด•เตเด• @@ -676,17 +775,17 @@ @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @joe:matrix.org Add - + เดšเต‡เตผเด•เตเด•เตเด• Invite - + เด•เตเดทเดฃเดฟเด•เตเด•เตเด• @@ -694,6 +793,32 @@ เดฑเดฆเตเดฆเดพเด•เตเด•เต + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + + + + + LeaveRoomDialog + + + Leave room + + + + + Are you sure you want to leave? + + + LoginPage @@ -704,7 +829,7 @@ e.g @joe:matrix.org - + เด‰เดฆเดพ. @joe:matrix.org @@ -753,28 +878,28 @@ Example: https://server.my:8787 LOGIN - + เดชเตเดฐเดตเต‡เดถเดฟเด•เตเด•เตเด• - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -784,42 +909,60 @@ Example: https://server.my:8787 - + An unknown error occured. Make sure the homeserver domain is valid. - + SSO LOGIN - + เดŽเดธเต เดŽเดธเต เด“ เดฒเต‹เด—เดฟเตป - + Empty password - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + Encryption enabled - + room name changed to: %1 removed room name - + เดฎเตเดฑเดฟเดฏเตเดŸเต† เดชเต‡เดฐเต เดจเต€เด•เตเด•เด‚ เดšเต†เดฏเตเดคเต @@ -829,7 +972,7 @@ Example: https://server.my:8787 removed topic - + เดตเดฟเดทเดฏเด‚ เดจเต€เด•เตเด•เด‚ เดšเต†เดฏเตเดคเต @@ -862,18 +1005,23 @@ Example: https://server.my:8787 - + + Allow them in + เด‡เดตเดฐเต† เด…เดจเตเดตเดฆเดฟเด•เตเด•เตเด• + + + %1 answered the call. - + removed เดจเต€เด•เตเด•เด‚เดšเต†เดฏเตโ€Œเดคเต - + %1 ended the call. @@ -893,7 +1041,7 @@ Example: https://server.my:8787 Send a file - + เด’เดฐเต เดซเดฏเตฝ เด…เดฏเดฏเตเด•เตเด•เตเด• @@ -901,9 +1049,9 @@ Example: https://server.my:8787 เด’เดฐเต เดธเดจเตเดฆเต‡เดถเด‚ เดŽเดดเตเดคเตเด•โ€ฆ. - + Stickers - + เดธเตเดฑเตเดฑเดฟเด•เตเด•เดฑเตเด•เตพ @@ -924,9 +1072,9 @@ Example: https://server.my:8787 MessageView - + Edit - + เดคเดฟเดฐเตเดคเตเดคเตเด• @@ -936,7 +1084,7 @@ Example: https://server.my:8787 Reply - + เดฎเดฑเตเดชเดŸเดฟ เดจเตฝเด•เตเด• @@ -944,17 +1092,19 @@ Example: https://server.my:8787 - + + &Copy - + + Copy &link location - + Re&act @@ -981,7 +1131,7 @@ Example: https://server.my:8787 &Mark as read - + &เดตเดพเดฏเดฟเดšเตเดšเดคเดพเดฏเดฟ เด•เดพเดฃเดฟเด•เตเด•เตเด• @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1059,7 +1219,7 @@ Example: https://server.my:8787 Deny - + เดจเดฟเดฐเดธเดฟเด•เตเด•เตเด• @@ -1069,6 +1229,14 @@ Example: https://server.my:8787 Accept + เดธเตเดตเต€เด•เดฐเดฟเด•เตเด•เตเด• + + + + NotificationWarning + + + You are about to notify the whole room @@ -1076,29 +1244,17 @@ Example: https://server.my:8787 NotificationsManager - - + + %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 เดฎเตˆเด•เตเดฐเต‹เดซเต‹เดฃเตŠเดจเตเดจเตเด‚ เด•เดฃเตเดŸเต†เดคเตเดคเดฟเดฏเดฟเดฒเตเดฒ. - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password เดชเดพเดธเตโ€เดตเต‡เดกเต @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 - + Password is not long enough (min 8 chars) - + Passwords don't match - + Invalid server name @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close เด…เดŸเดฏเตโ€Œเด•เตเด•เตเด• @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + เด…เดŸเดฏเตโ€Œเด•เตเด•เตเด• + + + Start a new chat @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 - + User settings @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1414,16 +1607,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1453,7 +1666,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1468,7 +1686,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1514,19 +1742,19 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 - + Select an avatar All Files (*) - + เดŽเดฒเตเดฒเดพ เดซเดฏเดฒเตเด•เดณเตเด‚ (*) @@ -1539,8 +1767,8 @@ Example: https://server.my:8787 - - + + Failed to upload image: %s @@ -1548,21 +1776,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1617,6 +1873,121 @@ Example: https://server.my:8787 เดฑเดฆเตเดฆเดพเด•เตเด•เต + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1669,18 +2040,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 - + Failed to encrypt event, sending aborted! - + Save image @@ -1700,7 +2071,7 @@ Example: https://server.my:8787 - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1709,7 +2080,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1719,7 +2090,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1739,12 +2120,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1754,12 +2135,12 @@ Example: https://server.my:8787 - + %1 was invited. - + %1 changed their avatar. @@ -1769,12 +2150,17 @@ Example: https://server.my:8787 - + %1 joined. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. @@ -1804,32 +2190,32 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. - + You joined this room. เดจเดฟเด™เตเด™เตพ เดˆ เดฎเตเดฑเดฟเดฏเดฟเตฝ เดšเต‡เตผเดจเตเดจเต. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. @@ -1848,7 +2234,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1856,12 +2242,17 @@ Example: https://server.my:8787 TimelineView - + No room open - + + No preview available + + + + %1 member(s) @@ -1886,28 +2277,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options @@ -1945,10 +2348,35 @@ Example: https://server.my:8787 + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1958,40 +2386,105 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar All Files (*) - + เดŽเดฒเตเดฒเดพ เดซเดฏเดฒเตเด•เดณเตเด‚ (*) @@ -2007,8 +2500,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2016,7 +2509,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2026,22 +2519,22 @@ Example: https://server.my:8787 - + Group's sidebar - + Circular Avatars - + profile: %1 - + Default @@ -2066,7 +2559,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2081,6 +2574,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2109,7 +2612,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts @@ -2175,7 +2678,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown @@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications @@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2414,17 +2957,17 @@ This usually causes the application icon in the task bar to animate in some fash All Files (*) - + เดŽเดฒเตเดฒเดพ เดซเดฏเดฒเตเด•เดณเตเด‚ (*) - + Open Sessions File - + @@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash - - + + File Password - + Enter the passphrase to decrypt the file: - + The password cannot be empty @@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2507,7 +3058,7 @@ This usually causes the application icon in the task bar to animate in some fash LOGIN - + เดชเตเดฐเดตเต‡เดถเดฟเด•เตเด•เตเด• @@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - เดฑเดฆเตเดฆเดพเด•เตเด•เต - - - - Room ID or alias - - - - - dialogs::LeaveRoom - - - Cancel - เดฑเดฆเตเดฆเดพเด•เตเด•เต - - - - Are you sure you want to leave? - - - dialogs::Logout @@ -2666,32 +3186,6 @@ Media size: %2 - - dialogs::ReadReceipts - - - Read receipts - - - - - Close - เด…เดŸเดฏเตโ€Œเด•เตเด•เตเด• - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2705,47 +3199,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2760,9 +3254,9 @@ Media size: %2 - + %1: %2 - + %1: %2 @@ -2780,27 +3274,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call เดจเดฟเด™เตเด™เตพ เด’เดฐเต เด•เต‹เตพ เด…เดตเดธเดพเดจเดฟเดชเตเดชเดฟเดšเตเดšเต - + %1 ended a call %1 เด’เดฐเต เด•เต‹เตพ เด…เดตเดธเดพเดจเดฟเดชเตเดชเดฟเดšเตเดšเต @@ -2808,7 +3302,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index b21db075..1ff88a61 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -4,35 +4,35 @@ ActiveCallBar - + Calling... - + Bellen... Connecting... - + Verbinden... You are screen sharing - + Scherm wordt gedeeld Hide/Show Picture-in-Picture - + Toon/verberg miniatuur Unmute Mic - + Microfoon aanzetten Mute Mic - + Microfoon dempen @@ -40,262 +40,264 @@ Awaiting Confirmation - + Wachten op bevestiging Waiting for other side to complete verification. - + Wachten op de andere kant om verificatie te voltooien. Cancel - Annuleren + Annuleren CallInvite - + Video Call - + Video oproep Voice Call - + Audio oproep No microphone found. - + Geen microfoon gevonden. CallInviteBar - + Video Call - + Video oproep Voice Call - + Audio oproep Devices - + Apparaten Accept - Accepteren + Aanvaarden Unknown microphone: %1 - + Onbekende microfoon: %1 Unknown camera: %1 - + Onbekende camera: %1 Decline - Afwijzen + Afwijzen No microphone found. - + Geen microfoon gevonden. CallManager - + Entire screen - + Gehele scherm ChatPage - + Failed to invite user: %1 Gebruiker uitnodigen mislukt: %1 - + Invited user: %1 Gebruiker uitgenodigd: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Het migreren can de cache naar de huidige versie is mislukt. Dit kan verscheidene redenen hebben. Maak a.u.b een issue aan en probeer in de tussentijd een oudere versie. Je kan ook proberen de cache handmatig te verwijderen. - + Confirm join - + Bevestig deelname Do you really want to join %1? - + Weet je zeker dat je %1 wil binnen gaan? - + Room %1 created. - Kamer %1 gecreรซerd. + Kamer %1 gemaakt. - + Confirm invite - + Bevestig uitnodiging - + Do you really want to invite %1 (%2)? - + Weet je zeker dat je %1 (%2) wil uitnodigen? - + Failed to invite %1 to %2: %3 - + Uitnodigen van %1 naar %2 mislukt: %3 Confirm kick - + Bevestig verwijdering Do you really want to kick %1 (%2)? - + Weet je zeker dat je %1 (%2) uit de kamer wil verwijderen? - + Kicked user: %1 - + Uit kamer verwijderde gebruiker: %1 Confirm ban - + Bevestig verbannen Do you really want to ban %1 (%2)? - + Weet je zeker dat je gebruiker %1 (%2) wil verbannen? - + Failed to ban %1 in %2: %3 - + Verbannen van %1 uit %2 mislukt: %3 Banned user: %1 - + Verbannen gebruiker: %1 Confirm unban - + Bevestig ongedaan maken verbanning Do you really want to unban %1 (%2)? - + Weet je zeker dat je %1 (%2) opnieuw wil toelaten? - + Failed to unban %1 in %2: %3 - + Opnieuw toelaten van %1 in %2 mislukt: %3 Unbanned user: %1 - + Toegelaten gebruiker: %1 - + Do you really want to start a private chat with %1? - + Weet je zeker dat je een privรฉ chat wil beginnen met %1? - + Cache migration failed! - + Migreren van de cache is mislukt! Incompatible cache version - + Incompatibele cacheversie The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache. - + De opgeslagen cache is nieuwer dan deze versie van Nheko ondersteunt. Update Nheko, of verwijder je cache. - + Failed to restore OLM account. Please login again. - + Herstellen van OLM account mislukt. Log a.u.b. opnieuw in. + + Failed to restore save data. Please login again. - + Opgeslagen gegevens herstellen mislukt. Log a.u.b. opnieuw in. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - + Instellen van de versleuteling is mislukt. Bericht van server: %1 %2. Probeer het a.u.b. later nog eens. - - + + Please try to login again: %1 - + Probeer a.u.b. opnieuw in te loggen: %1 - + Failed to join room: %1 - + Kamer binnengaan mislukt: %1 - + You joined the room - + Je bent de kamer binnengegaan. Failed to remove invite: %1 - + Uitnodiging verwijderen mislukt: %1 - + Room creation failed: %1 - + Kamer aanmaken mislukt: %1 Failed to leave room: %1 - + Kamer verlaten mislukt: %1 - + Failed to kick %1 from %2: %3 - + Kon %1 niet verwijderen uit %2: %3 @@ -303,7 +305,7 @@ Hide rooms with this tag or from this space by default. - + Verberg standaard kamers met deze markering of uit deze groep. @@ -311,70 +313,70 @@ All rooms - + Alle kamers Shows all rooms without filtering. - + Laat alles kamers zien zonder filters. Favourites - + Favorieten Rooms you have favourited. - + Je favoriete kamers. Low Priority - + Lage prioriteit Rooms with low priority. - + Kamers met lage prioriteit. Server Notices - + Serverberichten Messages from your server or administrator. - + Berichten van je server of beheerder. CrossSigningSecrets - + Decrypt secrets - + Ontsleutel geheimen Enter your recovery key or passphrase to decrypt your secrets: - + Voer je herstelsleutel of wachtwoordzin in om je geheimen te ontsleutelen: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Voer je herstelsleutel of wachtwoordzin in met de naam %1 om je geheimen te ontsleutelen: - + Decryption failed - + Ontsleutelen mislukt Failed to decrypt secrets with the provided recovery key or passphrase - + Geheimen konden niet worden ontsleuteld met de gegeven herstelsleutel of wachtwoordzin @@ -382,22 +384,22 @@ Verification Code - + Verificatiecode Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification! - + Controleer de volgende getallen. Je zou dezelfde getallen moeten zien aan beide kanten. Druk als ze verschillen op 'Ze komen niet overeen!' om de verificatie te annuleren! They do not match! - + Ze komen niet overeen! They match! - + Ze zijn gelijk! @@ -405,12 +407,12 @@ Apply - + Toepassen Cancel - Annuleren + Annuleren @@ -428,47 +430,47 @@ Search - + Zoeken - + People - + Mensen Nature - + Natuur Food - + Eten Activity - Activiteit + Activiteiten Travel - + Reizen Objects - Objecten + Objecten Symbols - Symbolen + Symbolen Flags - Vlaggen + Vlaggen @@ -476,22 +478,65 @@ Verification Code - + Verificatiecode Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification! - + Vergelijk de volgende emoji. Je zou dezelfde moeten zien aan beide kanten. Als ze verschillen, druk dan op 'Ze komen niet overeen!' om de verificatie te annuleren! They do not match! - + Ze komen niet overeen! They match! - + Ze zijn gelijk! + + + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Er is geen sleutel om dit bericht te ontsleutelen. We hebben de sleutel aangevraagd, maar je kan het opnieuw proberen als je ongeduldig bent. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Het bericht kon niet worden ontsleuteld, omdat we alleen een sleutel hebben voor nieuwere berichten. Je kan proberen toegang tot dit bericht aan te vragen. + + + + There was an internal error reading the decryption key from the database. + Er was een interne fout bij het lezen van de sleutel uit de database. + + + + There was an error decrypting this message. + Er was een fout bij het ontsleutelen van dit bericht. + + + + The message couldn't be parsed. + Het bericht kon niet worden verwerkt. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + De versleuteling was herbruikt! Wellicht probeert iemand vervalsde berichten in dit gesprek te injecteren! + + + + Unknown decryption error + Onbekende ontsleutelingsfout + + + + Request key + Vraag sleutel aan @@ -499,67 +544,22 @@ This message is not encrypted! - + Dit bericht is niet versleuteld! Encrypted by a verified device - + Versleuteld door een geverifieerd apparaat Encrypted by an unverified device, but you have trusted that user so far. - + Versleuteld door een ongeverifieerd apparaat, maar je hebt de gebruiker tot nu toe vertrouwd. - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- - + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Versleuteld door een ongeverifieerd apparaat of de sleutel komt van een niet te vertrouwen bron zoals een reservesleutel. @@ -567,33 +567,42 @@ Verification failed - + Verificatie mislukt Other client does not support our verification protocol. - + De andere kant ondersteunt ons verificatieprotocol niet. Key mismatch detected! + Verschil in sleutels gedetecteerd! + + + + Device verification timed out. + Apparaatverificatie is verlopen. + + + + Other party canceled the verification. + De andere kant heeft de verificatie geannuleerd. + + + + Verification messages received out of order! - - Device verification timed out. + Unknown verification error. - - Other party canceled the verification. - - - - + Close - + Sluiten @@ -601,7 +610,82 @@ Forward Message - + Bericht doorsturen + + + + ImagePackEditorDialog + + + Editing image pack + Afbeeldingspakket aanpassen + + + + Add images + Afbeeldingen toevoegen + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + Staatsleutel + + + + Packname + Afbeeldingspakketnaam + + + + Attribution + Bronvermelding + + + + + Use as Emoji + Gebruik als emoji + + + + + Use as Sticker + Gebruik als sticker + + + + Shortcode + Shortcode + + + + Body + Tekstinhoud + + + + Remove from pack + Verwijder uit afbeeldingspakket + + + + Remove + Verwijder + + + + Cancel + Annuleren + + + + Save + Opslaan @@ -609,89 +693,130 @@ Image pack settings - + Afbeeldingspakketinstellingen - + + Create account pack + Maak pakket voor je eigen account aan + + + + New room pack + Nieuw afbeeldingspakket voor kamer + + + Private pack - + Privรฉ afbeeldingspakket Pack from this room - + Afbeeldingspakket uit deze kamer Globally enabled pack - + Globaal geactiveerd afbeeldingspakket - + Enable globally - + Globaal activeren Enables this pack to be used in all rooms - + Activeert dit afbeeldingspakket voor gebruik in alle kamers - + + Edit + Bewerken + + + Close - + Sluiten InputBar - + Select a file - Kies een bestand + Selecteer een bestand All Files (*) - Alle bestanden (*) + Alle bestanden (*) - + Failed to upload media. Please try again. - + Het is niet is gelukt om de media te versturen. Probeer het a.u.b. opnieuw. InviteDialog - + Invite users to %1 - + Nodig gebruikers uit naar %1 User ID to invite - Uit te nodigen gebruikers-id + Gebruikers ID om uit te nodigen @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @jan:matrix.org Add - + Toevoegen Invite - + Uitnodigen Cancel - Annuleren + Annuleren + + + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + Kamer ID of alias + + + + LeaveRoomDialog + + + Leave room + Kamer verlaten + + + + Are you sure you want to leave? + Weet je zeker dat je de kamer wil verlaten? @@ -699,12 +824,12 @@ Matrix ID - Matrix-id + Matrix ID e.g @joe:matrix.org - b.v @jan:matrix.org< + bijv. @jan:matrix.org @@ -712,7 +837,10 @@ You can also put your homeserver address there, if your server doesn't support .well-known lookup. Example: @user:server.my If Nheko fails to discover your homeserver, it will show you a field to enter the server manually. - + Je inlognaam. Een mxid begint met @ gevolgd door de gebruikersnaam. Daarachter komt een dubbele punt (:) en de servernaam. +Je kan ook het adres van je thuisserver daar invoeren, als die geen .well-known ondersteund. +Voorbeeld: @gebruiker:mijnserver.nl +Als Nheko je thuisserver niet kan vinden, zal er een veld verschijnen om de server handmatig in te voeren. @@ -722,33 +850,34 @@ If Nheko fails to discover your homeserver, it will show you a field to enter th Your password. - + Je wachtwoord. Device name - + Apparaatnaam A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used. - + Een naam voor dit apparaat, welke zichtbaar zal zijn voor anderen als ze je apparaten verifiรซren. Als niets is ingevuld zal er een standaardnaam worden gebruikt. Homeserver address - + Thuisserveradres server.my:8787 - + mijnserver.nl:8787 The address that can be used to contact you homeservers client API. Example: https://server.my:8787 - + Het adres dat gebruikt kan worden om contact te zoeken met je thuisserver's gebruikers API. +Voorbeeld: https://mijnserver.nl:8787 @@ -756,126 +885,149 @@ Example: https://server.my:8787 INLOGGEN - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Je hebt een ongeldige Matrix ID ingevuld. Correct voorbeeld: @jan:matrix.org - + Autodiscovery failed. Received malformed response. - + Automatische herkenning mislukt. Ongeldig antwoord ontvangen. - + Autodiscovery failed. Unknown error when requesting .well-known. - + Automatische herkenning mislukt. Onbekende fout tijdens het opvragen van .well-known. - + The required endpoints were not found. Possibly not a Matrix server. - + De vereiste aanspreekpunten werden niet gevonden. Mogelijk geen Matrix server. Received malformed response. Make sure the homeserver domain is valid. - + Ongeldig antwoord ontvangen. Zorg dat de thuisserver geldig is. - + An unknown error occured. Make sure the homeserver domain is valid. - + Een onbekende fout trad op. Zorg dat de thuisserver geldig is. - + SSO LOGIN - + SSO INLOGGEN - + Empty password Leeg wachtwoord - + SSO login failed + SSO inloggen mislukt + + + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? MessageDelegate - + removed - + verwijderd Encryption enabled - + Versleuteling geactiveerd - + room name changed to: %1 - + kamernaam veranderd in: %1 removed room name - + kamernaam verwijderd topic changed to: %1 - + onderwerp aangepast naar: %1 removed topic - + onderwerp verwijderd %1 changed the room avatar - + %1 heeft de kameravatar veranderd %1 created and configured room: %2 - + %1 maakte en configureerde de kamer: %2 %1 placed a voice call. - + %1 plaatste een spraakoproep. %1 placed a video call. - + %1 plaatste een video oproep. %1 placed a call. - + %1 plaatste een oproep. %1 answered the call. - + %1 beantwoordde de oproep. %1 ended the call. - + %1 beรซindigde de oproep. Negotiating call... - + Onderhandelen oproepโ€ฆ + + + + Allow them in + Binnenlaten @@ -883,134 +1035,141 @@ Example: https://server.my:8787 Hang up - + Ophangen Place a call - + Plaats een oproep Send a file - + Verstuur een bestand Write a message... - Typ een bericht... + Typ een berichtโ€ฆ - + Stickers - + Stickers Emoji - + Emoji Send - + Verstuur You don't have permission to send messages in this room - + Je hebt geen toestemming om berichten te versturen in deze kamer MessageView - + Edit - + Bewerken React - + Reageren Reply - + Beantwoorden Options - + Opties - + + &Copy - + &Kopiรซren - + + Copy &link location - + Kopieer &link - + Re&act - + Re&ageren Repl&y - + Beantwoo&rden &Edit - + B&ewerken Read receip&ts - + Leesbeves&tigingen &Forward - + &Doorsturen &Mark as read - + Gelezen &markeren View raw message - + Ruw bericht bekijken View decrypted raw message - + Ontsleuteld ruw bericht bekijken Remo&ve message - + &Verwijder bericht &Save as - + Op&slaan als &Open in external program - + In extern programma &openen Copy link to eve&nt + Kopieer link naar gebeurte&nis + + + + &Go to quoted message @@ -1019,101 +1178,102 @@ Example: https://server.my:8787 Send Verification Request - + Verstuur verificatieverzoek Received Verification Request + Ontvangen verificatieverzoek + + + + 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.) - + 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? - + Om andere gebruikers te laten weten welke apparaten echt van jou zijn, kan je ze verifiรซren. Dit zorgt ook dat reservesleutels automatisch werken. Nu %1 verifiรซren? To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party. - + Om zeker te zijn dat niemand meeleest met je versleutelde gesprek kan je de andere kant verifiรซren. %1 has requested to verify their device %2. - + %1 heeft verzocht om hun apparaat %2 te verifiรซren. %1 using the device %2 has requested to be verified. - + %1, gebruikmakend van apparaat %2 heeft verzocht om verificatie. Your device (%1) has requested to be verified. - + Je apparaat (%1) heeft verzocht om verificatie. Cancel - Annuleren + Annuleren Deny - + Weigeren Start verification - + Begin verificatie Accept - Accepteren + Accepteren + + + + NotificationWarning + + + You are about to notify the whole room + NotificationsManager - - + + %1 sent an encrypted message - + %1 stuurde een versleuteld bericht - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - + %1 antwoordde: %2 %1 replied with an encrypted message - + %1 antwoordde met een versleuteld bericht %1 replied to a message - + %1 antwoordde op een bericht %1 sent a message - + %1 stuurde een bericht @@ -1121,32 +1281,32 @@ Example: https://server.my:8787 Place a call to %1? - + Bel %1? No microphone found. - + Geen microfoon gevonden. - + Voice - + Spraak Video - + Video Screen - + Scherm Cancel - Annuleren + Annuleren @@ -1154,49 +1314,65 @@ Example: https://server.my:8787 unimplemented event: - + Niet geรฏmplementeerd evenement: QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. - + Creรซer een uniek profiel, waardoor je op meerdere accounts tegelijk kan inloggen, en meerdere kopieรซn van Nheko tegelijk kan starten. profile - + profiel profile name - + profielnaam + + + + ReadReceipts + + + Read receipts + Leesbevestigingen + + + + ReadReceiptsModel + + + Yesterday, %1 + Gisteren, %1 RegisterPage - + Username Gebruikersnaam - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + De gebruikersnaam mag niet leeg zijn, en mag alleen de volgende tekens bevatten: a-z, 0-9, ., _, =, -, en /. - + Password Wachtwoord Please choose a secure password. The exact requirements for password strength may depend on your server. - + Kies a.u.b. een veilig wachtwoord. De exacte vereisten voor een wachtwoord kunnen per server verschillen. @@ -1206,12 +1382,12 @@ Example: https://server.my:8787 Homeserver - + Thuisserver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. - + Een server die registratie toestaat. Omdat Matrix gedecentraliseerd is, moet je eerst zelf een server vinden om je op te registeren, of je eigen server hosten. @@ -1219,52 +1395,42 @@ Example: https://server.my:8787 REGISTREREN - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Automatische herkenning mislukt. Onjuist gevormd antwoord ontvangen. - + Autodiscovery failed. Unknown error when requesting .well-known. - + Automatische herkenning mislukt. Onbekende fout bij opvragen van .well-known. - + The required endpoints were not found. Possibly not a Matrix server. - + De vereiste aanspreekpunten konden niet worden gevonden. Mogelijk geen Matrix server. Received malformed response. Make sure the homeserver domain is valid. - + Onjuist gevormd antwoord ontvangen. Zorg dat de thuisserver geldig is. An unknown error occured. Make sure the homeserver domain is valid. - + Een onbekende fout trad op. Zorg dat de thuisserver geldig is. - + Password is not long enough (min 8 chars) Het wachtwoord is niet lang genoeg (minimaal 8 tekens) - + Passwords don't match De wachtwoorden komen niet overeen - + Invalid server name Ongeldige servernaam @@ -1272,294 +1438,388 @@ Example: https://server.my:8787 ReplyPopup - + Close - + Sluiten Cancel edit + Bewerken annuleren + + + + RoomDirectory + + + Explore Public Rooms + Verken openbare kamers + + + + Search for public rooms + Zoek naar openbare kamers + + + + Choose custom homeserver RoomInfo - + no version stored - + geen versie opgeslagen RoomList - + New tag - + Nieuwe markering Enter the tag you want to use: - - - - - Leave Room - - - - - Are you sure you want to leave this room? - + Voer de markering in die je wil gebruiken: Leave room - Kamer verlaten + Kamer verlaten Tag room as: - + Markeer kamer als: Favourite - + Favoriet Low priority - + Lage prioriteit Server notice - + Serverbericht Create new tag... - + Maak nieuwe markering... - + Status Message - + Statusbericht Enter your status message: - + Voer je statusbericht in: Profile settings - + Profielinstellingen Set status message - + Stel statusbericht in - + Logout + Uitloggen + + + + Encryption not set up + Cross-signing setup has not run yet. - - Start a new chat + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Sluiten + + + + Start a new chat + Nieuwe chat beginnen + Join a room - Kamer betreden + Kamer binnengaan Create a new room - + Nieuwe kamer aanmaken Room directory - + Kamerlijst - + User settings - + Gebruikersinstellingen RoomMembers - + Members of %1 - + Deelnemers in %1 - + %n people in %1 Summary above list of members - - - + + %n persoon in %1 + %n personen in %1 Invite more people - + Nodig meer mensen uit + + + + This room is not encrypted! + Deze kamer is niet versleuteld! + + + + This user is verified. + Deze gebruiker is geverifieerd. + + + + This user isn't verified, but is still using the same master key from the first time you met. + Deze gebruiker is niet geverifieerd, maar gebruikt nog dezelfde hoofdsleutel als de eerste keer. + + + + This user has unverified devices! + Deze gebruiker heeft ongeverifieerde apparaten! RoomSettings - + Room Settings - + Kamerinstellingen - + %1 member(s) - + %1 deelnemer(s) SETTINGS - + INSTELLINGEN Notifications - + Meldingen Muted - + Gedempt Mentions only - + Alleen vermeldingen All messages - + Alle berichten - + + Room access + Kamertoegang + + + Anyone and guests - + Iedereen (inclusief gasten) Anyone - + Iedereen Invited users - + Uitgenodigde gebruikers - + + By knocking + Door aan te kloppen + + + + Restricted by membership in other rooms + Beperkt door deelname aan andere kamers + + + Encryption - + Versleuteling End-to-End Encryption - + Eind-tot-eind versleuteling Encryption is currently experimental and things might break unexpectedly. <br> Please take note that it can't be disabled afterwards. - + Versleuteling is momenteel experimenteel en dingen kunnen onverwacht stuk gaan.<br>Let op: versleuteling kan achteraf niet uitgeschakeld worden. Sticker & Emote Settings - + Sticker & Emoji instellingen Change - + Bewerken Change what packs are enabled, remove packs or create new ones - + Verander welke afbeeldingspakketten zijn ingeschakeld, verwijder ze of voeg nieuwe toe INFO - + INFO Internal ID - + Interne ID Room Version - + Kamerversie - + Failed to enable encryption: %1 - + Versleuteling kon niet worden ingeschakeld: %1 - + Select an avatar - + Kies een avatar All Files (*) - Alle bestanden (*) + Alle bestanden (*) The selected file is not an image - + Het gekozen bestand is geen afbeelding Error while reading file: %1 - + Fout bij lezen van bestand: %1 - - + + Failed to upload image: %s - + Uploaden van afbeelding mislukt: %1 RoomlistModel - + Pending invite. - + Wachtende uitnodiging. - + Previewing this room + Voorbeeld van deze kamer + + + + No preview available + Geen voorbeeld beschikbaar + + + + Root + + + Please enter your login password to continue: - - No preview available + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. @@ -1568,53 +1828,168 @@ Example: https://server.my:8787 Share desktop with %1? - + Scherm delen met %1? Window: - + Scherm: Frame rate: - + Verversingssnelheid: Include your camera picture-in-picture - + Laat eigen cameraminiatuur zien Request remote camera - + Verzoek om camera van de andere kant View your callee's camera like a regular video call - + Bekijk de camera van degene die belt zoals bij een regulier videogesprek Hide mouse cursor - + Verstop muiscursor Share - + Delen Preview - + Voorbeeld Cancel - Annuleren + Annuleren + + + + SecretStorage + + + Failed to connect to secret storage + Verbinden met geheimopslag mislukt + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Kon afbeeldingspakket niet updaten: %1 + + + + Failed to delete old image pack: %1 + Kon oud afbeeldingspakket niet verwijderen: %1 + + + + Failed to open image: %1 + Kon afbeelding niet openen: %1 + + + + Failed to upload image: %1 + Kon afbeelding niet uploaden: %1 @@ -1622,22 +1997,22 @@ Example: https://server.my:8787 Failed - + Mislukt Sent - + Verstuurd Received - + Ontvangen Read - + Gelezen @@ -1645,7 +2020,7 @@ Example: https://server.my:8787 Search - + Zoeken @@ -1653,283 +2028,315 @@ Example: https://server.my:8787 Successful Verification - + Succesvolle verificatie Verification successful! Both sides verified their devices! - + Verificatie gelukt! Beide kanten hebben hun apparaat geverifieerd! Close - + Sluiten TimelineModel - + Message redaction failed: %1 - + Bericht intrekken mislukt: %1 - + Failed to encrypt event, sending aborted! - + Kon evenement niet versleutelen, versturen geannuleerd! - + Save image - Afbeelding opslaan + Afbeelding opslaan Save video - + Video opslaan Save audio - + Audio opslaan Save file - + Bestand opslaan - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) - - - + + %1%2 is aan het typen. + %1 en %2 zijn aan het typen. - + %1 opened the room to the public. - + %1 maakte de kamer openbaar. %1 made this room require and invitation to join. - + %1 maakte deze kamer uitnodiging-vereist. - + + %1 allowed to join this room by knocking. + %1 maakte deze kamer aanklopbaar. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 stond toe dat deelnemers aan de volgende kamers automatisch mogen deelnemen: %2 + + + %1 made the room open to guests. - + %1 maakte de kamer openbaar voor gasten. %1 has closed the room to guest access. - + %1 maakte de kamer gesloten voor gasten. %1 made the room history world readable. Events may be now read by non-joined people. - + %1 maakte de kamergeschiedenis openbaar. Niet-deelnemers kunnen nu de kamer inzien. %1 set the room history visible to members from this point on. - + %1 maakte de kamergeschiedenis deelname-vereist. - + %1 set the room history visible to members since they were invited. - + %1 maakte de kamergeschiedenis zichtbaar vanaf het moment van uitnodigen. - + %1 set the room history visible to members since they joined the room. - + %1 maakte de kamergeschiedenis zichtbaar vanaf moment van deelname. %1 has changed the room's permissions. - + %1 heeft de rechten van de kamer aangepast. - + %1 was invited. - + %1 is uitgenodigd. - + %1 changed their avatar. - + %1 is van avatar veranderd. %1 changed some profile info. - + %1 heeft wat profielinformatie aangepast. - + %1 joined. - + %1 neemt nu deel. - + + %1 joined via authorisation from %2's server. + %1 neemt deel via autorisatie van %2's server. + + + %1 rejected their invite. - + %1 heeft de uitnodiging geweigerd. Revoked the invite to %1. - + Uitnodiging van %1 is ingetrokken. %1 left the room. - + %1 heeft de kamer verlaten. Kicked %1. - + %1 is verwijderd. Unbanned %1. - + %1 is opnieuw toegelaten. %1 was banned. - + %1 is verbannen. - + Reason: %1 - + Reden: %1 - + %1 redacted their knock. - + %1 heeft het aankloppen ingetrokken. - + You joined this room. - Je bent lid geworden van deze kamer. + Je neemt nu deel aan deze kamer. - + %1 has changed their avatar and changed their display name to %2. - + %1 is van avatar veranderd en heet nu %2. - + %1 has changed their display name to %2. - + %1 heet nu %2. - + Rejected the knock from %1. - + Aankloppen van %1 geweigerd. %1 left after having already left! This is a leave event after the user already left and shouldn't happen apart from state resets - + %1 is vertrokken na reeds vertrokken te zijn! %1 knocked. - + %1 klopt aan. TimelineRow - + Edited - + Bewerkt TimelineView - + No room open - + Geen kamer open - + + No preview available + Geen voorbeeld beschikbaar + + + %1 member(s) - + %1 deelnemer(s) join the conversation - + Neem deel aan het gesprek accept invite - + accepteer uitnodiging decline invite - + wijs uitnodiging af Back to room list - - - - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - + Terug naar kamerlijst TopBar - + Back to room list - + Terug naar kamerlijst - + No room selected + Geen kamer geselecteerd + + + + This room is not encrypted! + Deze kamer is niet versleuteld! + + + + This room contains only verified devices. + Deze kamer bevat alleen geverifieerde apparaten. + + + + This room contains verified devices and devices which have never changed their master key. - + + This room contains unverified devices! + Deze kamer bevat ongeverifieerde apparaten! + + + Room options - + Kameropties Invite users - Gebruikers uitnodigen + Gebruikers uitnodigen Members - Leden + Deelnemers Leave room - Kamer verlaten + Kamer verlaten Settings - Instellingen + Instellingen @@ -1945,78 +2352,168 @@ Example: https://server.my:8787 Afsluiten + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Voer a.u.b een geldig registratieteken in. + + + + Invalid token + + + UserProfile - + Global User Profile - + Globaal gebruikersprofiel Room User Profile - + Kamerspecifiek gebruikersprofiel - - + + Change avatar globally. + Verander avatar globaal. + + + + Change avatar. Will only apply to this room. + Verander avatar. Heeft alleen effect op deze kamer. + + + + Change display name globally. + Verander weergavenaam globaal. + + + + Change display name. Will only apply to this room. + Verander weergavenaam. Heeft alleen effect op deze kamer. + + + + Room: %1 + Kamer: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + Dit is een kamer-specifiek profiel. De weergavenaam en avatar kunnen verschillen van de globale versie. + + + + Open the global profile for this user. + Open het globale profiel van deze gebruiker. + + + + Verify - + Verifiรซren - - Ban the user - - - - - Start a private chat - + + Start a private chat. + Begin een privรฉchat. - Kick the user + Kick the user. + Verwijder de gebruiker. + + + + Ban the user. + Verban de gebruiker. + + + + Refresh device list. - + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify + On-verifiรซren + + + + Sign out device %1 - - Select an avatar + + You signed out this device. + + + Select an avatar + Kies een avatar + All Files (*) - Alle bestanden (*) + Alle bestanden (*) The selected file is not an image - + Het gekozen bestand is geen afbeelding Error while reading file: %1 - + Fout bij lezen bestand: %1 UserSettings - - + + Default - + Standaard UserSettingsPage - + Minimize to tray Minimaliseren naar systeemvak @@ -2026,145 +2523,163 @@ Example: https://server.my:8787 Geminimaliseerd opstarten - + Group's sidebar - Zijbalk van groep + Zijbalk met groepen - + Circular Avatars - + Ronde avatars - + profile: %1 - + profiel: %1 - + Default - + Standaard CALLS - + OPROEPEN Cross Signing Keys - + Kruisversleutelingssleutels REQUEST - + OPVRAGEN DOWNLOAD - + DOWNLOADEN - + Keep the application running in the background after closing the client window. - + Blijf draaien in de achtergrond na het sluiten van het scherm. Start the application in the background without showing the client window. - + Start de applicatie in de achtergrond zonder het scherm te tonen. Change the appearance of user avatars in chats. OFF - square, ON - Circle. + Verander het uiterlijk van avatars in de chats. +UIT - vierkant, AAN - cirkel. + + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. Show a column containing groups and tags next to the room list. - + Laat een kolom zien met groepen en markeringen naast de kamerlijst. Decrypt messages in sidebar - + Ontsleutel berichten in de zijbalk Decrypt the messages shown in the sidebar. Only affects messages in encrypted chats. - + Ontsleutel de berichten getoond in de zijbalk. +Heeft alleen effect op versleutelde chats. Privacy Screen - + Privacy scherm When the window loses focus, the timeline will be blurred. - + Als het scherm focus verliest, zal de tijdlijn +worden geblurt. - + Privacy screen timeout (in seconds [0 - 3600]) - + Privacy scherm wachttijd (in seconden [0 - 3600]) Set timeout (in seconds) for how long after window loses focus before the screen will be blurred. Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) - + Stel wachttijd (in seconden) voor hoe lang het duurt nadat +focus weg is voordat het scherm wordt geblurt. +Kies 0 om direct te blurren. Maximale waarde is 1 uur (3600 seconden) Show buttons in timeline - + Laat knoppen zien in tijdlijn Show buttons to quickly reply, react or access additional options next to each message. - + Laat knoppen zien om snel te reageren, beantwoorden, of extra opties te kunnen gebruiken naast elk bericht. Limit width of timeline - + Beperk breedte van tijdlijn Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised - + Stel de maximale breedte in van berichten in de tijdlijn (in pixels). Dit kan helpen bij de leesbaarheid als Nheko gemaximaliseerd is. Typing notifications - Meldingen bij typen van berichten + Typnotificaties Show who is typing in a room. This will also enable or disable sending typing notifications to others. - + Laat zien wie er typt in een kamer. +Dit schakelt ook het versturen van je eigen typnotificaties naar anderen in of uit. Sort rooms by unreads - + Sorteer kamers op ongelezen berichten Display rooms with new messages first. If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room. If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. - + Laat kamers met nieuwe berichten eerst zien. +Indien uitgeschakeld, staan kamers gesorteerd op de tijd van het laatst ontvangen bericht. +Indien ingeschakeld, staan kamers met actieve notificaties (het cirkeltje met een getal erin) bovenaan. Kamers die je hebt gedempt zullen nog steeds op tijd zijn gesorteerd, want die vind je blijkbaar niet zo belangrijk als de andere kamers. - + Read receipts Leesbevestigingen @@ -2172,94 +2687,137 @@ If this is on, rooms which have active notifications (the small circle with a nu Show if your message was read. Status is displayed next to timestamps. - + Laat zien of je bericht gelezen is. +De status staat naast de tijdsindicatie. - + Send messages as Markdown - + Verstuur berichten in Markdown Allow using markdown in messages. When disabled, all messages are sent as a plain text. - + Sta het gebruik van Markdown in berichten toe. +Indien uitgeschakeld worden alle berichten als platte tekst verstuurd. + Play animated images only on hover + Speel animaties in afbeeldingen alleen af tijdens muisover + + + Desktop notifications - + Bureaubladnotificaties Notify about received message when the client is not currently focused. - + Verstuur een notificatie over ontvangen berichten als het scherm geen focus heeft. Alert on notification - + Melding bij notificatie Show an alert when a message is received. This usually causes the application icon in the task bar to animate in some fashion. - + Activeer een melding als een bericht binnen komt. +Meestal zorgt dit dat het icoon in de taakbalk op een manier animeert of iets dergelijks. Highlight message on hover - + Oplichten van berichten onder muis Change the background color of messages when you hover over them. - + Veranderd de achtergrondkleur van het bericht waar de muiscursor op staat. Large Emoji in timeline - + Grote emoji in de tijdlijn Make font size larger if messages with only a few emojis are displayed. - + Maakt het lettertype groter als berichten met slechts enkele emoji worden getoond. - + + Send encrypted messages to verified users only + Verstuur alleen versleutelde berichten naar geverifieerde gebruikers + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Vereist dat een gebruiker geverifieerd is voordat berichten worden versleuteld. Verbetert de beveiliging maar maakt versleutelen irritanter om in te stellen. + + + Share keys with verified users and devices - + Deel sleutels met geverifieerde gebruikers en apparaten - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Beantwoord automatisch sleutelverzoeken van andere gebruikers, indien geverifieerd, ook als dat apparaat normaal geen toegang tot die sleutels had moeten hebben. + + + + Online Key Backup + Online reservesleutel + + + + Download message encryption keys from and upload to the encrypted online key backup. + Download van en upload naar de online reservesleutel. + + + + Enable online key backup + Activeer online reservesleutelopslag + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + De Nheko auteurs raden af om online reservesleutelopslag te gebruiken totdat symmetrische reservesleutelopslag beschikbaar is. Toch activeren? + + + CACHED - + IN CACHE NOT CACHED - + NIET IN CACHE - + Scale factor - + Schaalfactor Change the scale factor of the whole user interface. - + Verander de schaalfactor van de gehele gebruikersinterface. Font size - + Lettertypegrootte Font Family - + Lettertype @@ -2269,194 +2827,202 @@ This usually causes the application icon in the task bar to animate in some fash Ringtone - + Beltoon Set the notification sound to play when a call invite arrives - + Stel het geluid in dat speelt als een oproep binnen komt Microphone - + Microfoon Camera - + Camera Camera resolution - + Cameraresolutie Camera frame rate - + Cameraverversingssnelheid Allow fallback call assist server - + Sta terugval naar oproepassistentieserver toe Will use turn.matrix.org as assist when your home server does not offer one. - + Zal turn.matrix.org gebruiken om te assisteren als je thuisserver geen TURN server heeft. Device ID - + Apparaat ID Device Fingerprint - + Apparaat vingerafdruk - + Session Keys - + Sessiesleutels IMPORT - + IMPORTEREN EXPORT - + EXPORTEREN ENCRYPTION - + VERSLEUTELING - + GENERAL ALGEMEEN - + INTERFACE - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Speelt media zoals GIFs en WebPs alleen af terwijl de muiscursor erboven hangt. + + + Touchscreen mode - + Touchscreenmodus Will prevent text selection in the timeline to make touch scrolling easier. - + Voorkomt dat tekst geselecteerd wordt in de tijdlijn, om scrollen makkelijker te maken. Emoji Font Family - + Emoji lettertype - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key - + Hoofdsleutel Your most important key. You don't need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys. - + Je belangrijkste sleutel. Deze hoeft niet gecached te zijn, en dat maakt het minder waarschijnlijk dat hij ooit gestolen wordt. Hij is alleen nodig om je andere sleutels te roteren. User signing key - + Gebruikerssleutel The key to verify other users. If it is cached, verifying a user will verify all their devices. - + De sleutel die wordt gebruikt om andere gebruikers te verifiรซren. Indien gecached zal het verifiรซren van een gebruiker alle apparaten van die gebruiker verifiรซren. - + Self signing key - + Zelf ondertekenen sleutel The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you. - + De sleutel om je eigen apparaten mee te verifiรซren. Indien gecached zal รฉรฉn van je apparaten verifiรซren dat doen voor andere apparaten en gebruikers die jou geverifieerd hebben. Backup key - + Reservesleutel The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server. - + De sleutel om online reservesleutels mee te ontsleutelen. Indien gecached kan je online reservesleutel activeren om je sleutels veilig versleuteld op de server op te slaan. Select a file - Kies een bestand + Kies een bestand All Files (*) - Alle bestanden (*) + Alle bestanden (*) - + Open Sessions File - + Open sessiebestand - + Error - + Fout - - + + File Password - + Wachtwoord voor bestand - + Enter the passphrase to decrypt the file: - + Voer de wachtwoordzin in om het bestand te ontsleutelen: - + The password cannot be empty - + Het wachtwoord kan niet leeg zijn Enter passphrase to encrypt your session keys: - + Voer wachtwoordzin in om je sessiesleutels mee te versleutelen: File to save the exported session keys - + Bestand om geรซxporteerde sessiesleutels in op te slaan + + + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Geen versleutelde chat gevonden met deze gebruiker. Maak een versleutelde chat aan met deze gebruiker en probeer het opnieuw. @@ -2464,27 +3030,27 @@ This usually causes the application icon in the task bar to animate in some fash Waiting for other partyโ€ฆ - + Wachten op andere kantโ€ฆ Waiting for other side to accept the verification request. - + Wachten op de andere kant om het verificatieverzoek te accepteren. Waiting for other side to continue the verification process. - + Wachten op de andere kant om het verificatieproces voort te zetten. Waiting for other side to complete the verification process. - + Wachten op de andere kant om het verificatieproces af te ronden. Cancel - Annuleren + Annuleren @@ -2492,7 +3058,7 @@ This usually causes the application icon in the task bar to animate in some fash Welcome to nheko! The desktop client for the Matrix protocol. - Welkom bij nheko! Dรฉ computerclient voor het Matrix-protocol. + Welkom bij Nheko! De bureaubladclient voor het Matrix-protocol. @@ -2515,7 +3081,7 @@ This usually causes the application icon in the task bar to animate in some fash Yesterday - + Gisteren @@ -2523,12 +3089,12 @@ This usually causes the application icon in the task bar to animate in some fash Create room - + Kamer maken Cancel - Annuleren + Annuleren @@ -2553,7 +3119,7 @@ This usually causes the application icon in the task bar to animate in some fash Room Preset - Kamer-voorinstellingen + Kamer voorinstellingen @@ -2566,53 +3132,22 @@ This usually causes the application icon in the task bar to animate in some fash Open Fallback in Browser - + Open fallback in browser Cancel - Annuleren + Annuleren Confirm - + Bevestigen Open the fallback, follow the steps and confirm after completing them. - - - - - dialogs::JoinRoom - - - Join - - - - - Cancel - Annuleren - - - - Room ID or alias - Kamer-id of alias - - - - dialogs::LeaveRoom - - - Cancel - Annuleren - - - - Are you sure you want to leave? - Weet je zeker dat je wilt vertrekken? + Open de fallback, volg de stappen, en bevestig nadat je klaar bent. @@ -2620,7 +3155,7 @@ This usually causes the application icon in the task bar to animate in some fash Cancel - Annuleren + Annuleren @@ -2645,7 +3180,7 @@ This usually causes the application icon in the task bar to animate in some fash Media type: %1 Media size: %2 - Mediasoort: %1 + Mediatype: %1 Mediagrootte: %2 @@ -2655,12 +3190,12 @@ Mediagrootte: %2 Cancel - Annuleren + Annuleren Confirm - + Bevestigen @@ -2668,151 +3203,125 @@ Mediagrootte: %2 Los de reCAPTCHA op en klik op 'Bevestigen' - - dialogs::ReadReceipts - - - Read receipts - Leesbevestigingen - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: You sent an audio clip - + Je verstuurde een audio clip %1 sent an audio clip - + %1 verstuurde een audio clip - + You sent an image - + Je verstuurde een afbeelding - + %1 sent an image - + %1 verstuurde een afbeelding - + You sent a file - + Je verstuurde een bestand - + %1 sent a file - + %1 verstuurde een bestand - + You sent a video - + Je verstuurde een video - + %1 sent a video - + %1 verstuurde een video - + You sent a sticker - + Je verstuurde een sticker - + %1 sent a sticker - + %1 verstuurde een sticker - + You sent a notification - + Je verstuurde een notificatie %1 sent a notification - + %1 verstuurde een notificatie You: %1 - + Jij: %1 - + %1: %2 - + %1: %2 You sent an encrypted message - + Je hebt een versleuteld bericht verstuurd %1 sent an encrypted message - + %1 heeft een versleuteld bericht verstuurd You placed a call - + Je hebt een oproep geplaatst - + %1 placed a call - + %1 plaatste een oproep - + You answered a call - + Je beantwoordde een oproep - + %1 answered a call - + %1 beantwoordde een oproep - + You ended a call - + Je hing op - + %1 ended a call - + %1 hing op utils - + Unknown Message Type - + Onbekend berichttype diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index 72e4e771..0f625d44 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -4,25 +4,25 @@ ActiveCallBar - + Calling... - + Connecting... - + ลฤ…czenie... You are screen sharing - + Udostฤ™pniasz ekran Hide/Show Picture-in-Picture - + Ukryj/Pokaลผ Obraz w obrazie @@ -56,120 +56,120 @@ CallInvite - + Video Call - + Poล‚ฤ…czenie Wideo Voice Call - + Poล‚ฤ…czenie Gล‚osowe No microphone found. - + Nie wykryto mikrofonu. CallInviteBar - + Video Call - + Poล‚ฤ…czenie Wideo Voice Call - + Poล‚ฤ…czenie Gล‚osowe Devices - Urzฤ…dzenia + Urzฤ…dzenia Accept - Akceptuj + Akceptuj Unknown microphone: %1 - + Nieznany mikrofon: %1 Unknown camera: %1 - + Nieznana kamera: %1 Decline - Odrzuฤ‡ + Odrzuฤ‡ No microphone found. - + Nie znaleziono mikrofonu. CallManager - + Entire screen - + Caล‚y ekran ChatPage - + Failed to invite user: %1 Nie udaล‚o siฤ™ zaprosiฤ‡ uลผytkownika: %1 - + Invited user: %1 - + Zaproszono uลผytkownika %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Migracja cachu do obecnej wersji nieudana. Przyczyny mogฤ… byฤ‡ rรณลผne. Proszฤ™ zgล‚osiฤ‡ bล‚ฤ…d i w miedzyczasie uลผywaฤ‡ starszej wersji. Moลผesz rรณwnieลผ sprรณbuwaฤ‡ usunฤ…ฤ‡ cache rฤ™cznie. - + Confirm join - + Potwierdลบ doล‚ฤ…czenie Do you really want to join %1? - + Czy na pewno chcesz doล‚ฤ…czyฤ‡ do %1? - + Room %1 created. - + Utworzono pokรณj %1. - + Confirm invite - + Potwierdลบ zaproszenie - + Do you really want to invite %1 (%2)? Czy na pewno chcesz zaprosiฤ‡ %1 (%2)? - + Failed to invite %1 to %2: %3 - + Zaproszenie %1 do %2 nieudane: %3 @@ -182,7 +182,7 @@ czy na pewno chcesz wykopaฤ‡ %1 (%2)? - + Kicked user: %1 Wykopano uลผytkownika: %1 @@ -197,9 +197,9 @@ Czy na pewno chcesz zablokowaฤ‡ %1 (%2)? - + Failed to ban %1 in %2: %3 - + Nie udaล‚o siฤ™ zbanowaฤ‡ %1 w %2: %3 @@ -217,9 +217,9 @@ Czy na pewno chcesz odblokowaฤ‡ %1 (%2)? - + Failed to unban %1 in %2: %3 - + Nie udaล‚o siฤ™ odbanowaฤ‡ %1 w %2: %3 @@ -227,12 +227,12 @@ Odblokowano uลผytkownika: %1 - + Do you really want to start a private chat with %1? - + Czy na pewno chcesz rozpoczฤ…ฤ‡ prywatny czat z %1? - + Cache migration failed! Nie udaล‚o siฤ™ przenieล›ฤ‡ pamiฤ™ci podrฤ™cznej! @@ -247,33 +247,35 @@ Pamiฤ™ฤ‡ podrฤ™czna na Twoim dysku jest nowsza niลผ wersja obsล‚ugiwana przez Nheko. Zaktualizuj lub wyczyล›ฤ‡ pamiฤ™ฤ‡ podrฤ™cznฤ…. - + Failed to restore OLM account. Please login again. Nie udaล‚o siฤ™ przywrรณciฤ‡ konta OLM. Sprรณbuj zalogowaฤ‡ siฤ™ ponownie. + + Failed to restore save data. Please login again. Nie udaล‚o siฤ™ przywrรณciฤ‡ zapisanych danych. Sprรณbuj zalogowaฤ‡ siฤ™ ponownie. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Nie udaล‚o siฤ™ ustawiฤ‡ kluczy szyfrujฤ…cych. Odpowiedลบ serwera: %1 %2. Sprรณbuj ponownie pรณลบniej. - - + + Please try to login again: %1 Sprรณbuj zalogowaฤ‡ siฤ™ ponownie: %1 - + Failed to join room: %1 Nie udaล‚o siฤ™ doล‚ฤ…czyฤ‡ do pokoju: %1 - + You joined the room Doล‚ฤ…czyล‚eล› do pokoju @@ -283,7 +285,7 @@ Nie udaล‚o siฤ™ usunฤ…ฤ‡ zaproszenia: %1 - + Room creation failed: %1 Tworzenie pokoju nie powiodล‚o siฤ™: %1 @@ -293,9 +295,9 @@ Nie udaล‚o siฤ™ opuล›ciฤ‡ pokoju: %1 - + Failed to kick %1 from %2: %3 - + Nie udaล‚o siฤ™ wykopaฤ‡ %1 z %2: %3 @@ -303,7 +305,7 @@ Hide rooms with this tag or from this space by default. - + Domyล›lnie ukryj pokoje oznaczone tym tagiem z tej przestrzeni. @@ -311,70 +313,70 @@ All rooms - Wszystkie pokoje + Wszystkie pokoje Shows all rooms without filtering. - + Pokazuj wszystkie pokoje bez filtrowania. Favourites - + Ulubione Rooms you have favourited. - + Pokoje dodane przez ciebie do ulubionych. Low Priority - + Niski Priorytet Rooms with low priority. - + Pokoje o niskim priorytecie. Server Notices - Ogล‚oszenia serwera + Ogล‚oszenia Serwera Messages from your server or administrator. - + Wiadomoล›ci od twojego serwera lub administratora. CrossSigningSecrets - + Decrypt secrets - + Odszyfruj sekrety Enter your recovery key or passphrase to decrypt your secrets: - + Wprowadลบ swรณj klucz odzyskiwania lub frazฤ™-klucz by odszyfrowaฤ‡ swoje sekrety: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Wprowadลบ swรณj klucz odzyskiwania lub frazฤ™ klucz nazwanฤ…: %1 by odszyfrowaฤ‡ swoje sekrety: - + Decryption failed - + Odszyfrowywanie nieudane Failed to decrypt secrets with the provided recovery key or passphrase - + Nie udaล‚o siฤ™ odszyfrowaฤ‡ sekretรณw przy pomocy podanego klucza odzyskiwania albo frazy-klucz @@ -431,7 +433,7 @@ Szukaj - + People Ludzie @@ -448,7 +450,7 @@ Activity - Aktywnoล›ฤ‡ + Aktywnoล›ฤ‡ @@ -458,17 +460,17 @@ Objects - Przedmioty + Przedmioty Symbols - Symbole + Symbole Flags - Flagi + Flagi @@ -494,6 +496,49 @@ Pasujฤ…! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Brakuje klucza do odblokowania tej wiadomoล›ci. Poprosiliล›my o klucz automatycznie, ale moลผesz poprosiฤ‡ rฤ™cznie jeszcze raz, jeล›li jesteล› niecierpliwy(a). + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Ta wiadomoล›ฤ‡ nie mogล‚a zostaฤ‡ odszyfrowana, poniewaลผ mamy klucz wyล‚ฤ…cznie dla nowszych wiadomoล›ci. Moลผesz sprรณbowaฤ‡ poprosiฤ‡ o dostฤ™p do tej wiadomoล›ci. + + + + There was an internal error reading the decryption key from the database. + Wystฤ…piล‚ wewnฤ™trzny bล‚ฤ…d podczas prรณby odczytu klucza do odszyfrowywania z bazy danych. + + + + There was an error decrypting this message. + Wystฤ…piล‚ bล‚ฤ…d podczas odszyfrowywania tej wiadomoล›ci. + + + + The message couldn't be parsed. + Wystฤ…piล‚ bล‚ฤ…d podczas przetwarzania tej wiadomoล›ci. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + Ten klucz szyfrowania zostaล‚ juลผ uลผyty! Byฤ‡ moลผe ktoล› prรณbuje umieล›ciฤ‡ faล‚szywe wiadomoล›ci w tym czacie! + + + + Unknown decryption error + Niezidentyfikowany bล‚ฤ…d odszyfrowywania + + + + Request key + Poproล› o klucz + + EncryptionIndicator @@ -504,62 +549,17 @@ Encrypted by a verified device - + Zaszyfrowane przez zweryfikowane urzฤ…dzenie Encrypted by an unverified device, but you have trusted that user so far. - + Zaszyfrowane przez niezweryfikowane urzฤ…dzenie, ale pochodzฤ…ce od zaufanego uลผytkownika. - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Zdarzenie szyfrowania (Nie znaleziono kluczy deszyfrujฤ…cych) - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - --Bล‚ฤ…d deszyfrowania (nie udaล‚o siฤ™ uzyskaฤ‡ kluczy megolm z bazy danych) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Bล‚ฤ…d Deszyfracji (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Zdarzenie szyfrowania (Nieznany typ zdarzenia) -- - - - - -- Replay attack! This message index was reused! -- - -- Atak powtรณrzeniowy! Indeks tej wiadomoล›ci zostaล‚ uลผyty ponownie! -- - - - - -- Message by unverified device! -- - -- Wiadomoล›ฤ‡ z niezweryfikowanego urzฤ…dzenia! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Zaszyfrowane przez niezweryfikowane urzฤ…dzenie, albo klucz pochodzi z niezaufanego ลบrรณdล‚a, np. backup-u klucza. @@ -581,17 +581,26 @@ - Device verification timed out. Przekroczono limit czasu na weryfikacjฤ™ urzฤ…dzenia. - + Other party canceled the verification. Druga strona anulowaล‚a weryfikacjฤ™. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close Zamknij @@ -601,7 +610,82 @@ Forward Message - + Przeล›lij wiadomoล›ฤ‡ dalej + + + + ImagePackEditorDialog + + + Editing image pack + Edytowanie paczki obrazรณw + + + + Add images + Dodaj obrazy + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Naklejki (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + Unikalny klucz paczki + + + + Packname + Nazwa paczki + + + + Attribution + ลนrรณdล‚o (autor/link) + + + + + Use as Emoji + Uลผyj jako Emoji + + + + + Use as Sticker + Uลผyj jako Naklejki + + + + Shortcode + Skrรณt + + + + Body + Treล›ฤ‡ + + + + Remove from pack + Usuล„ z paczki + + + + Remove + Usuล„ + + + + Cancel + Anuluj + + + + Save + Zapisz @@ -609,89 +693,130 @@ Image pack settings - + Ustawienia paczki obrazรณw - + + Create account pack + Utwรณrz paczkฤ™ konta + + + + New room pack + Nowa paczka pokoju + + + Private pack - + Prywatna paczka Pack from this room - + Paczka z tego pokoju Globally enabled pack - + Paczka wล‚ฤ…czona globalnie - + Enable globally - + Wล‚ฤ…cz globalnie Enables this pack to be used in all rooms - + Umoลผliw uลผywanie tej paczki we wszystkich pokojach - + + Edit + Edytuj + + + Close - Zamknij + Zamknij InputBar - + Select a file - Wybierz plik + Wybierz plik All Files (*) - Wszystkie pliki (*) + Wszystkie pliki (*) - + Failed to upload media. Please try again. - + Wysล‚anie mediรณw nie powiodล‚o siฤ™. Sprรณbuj ponownie. InviteDialog - + Invite users to %1 - + Zaproล› uลผytkownikรณw do %1 User ID to invite - ID uลผytkownika do zaproszenia + ID uลผytkownika do zaproszenia @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @ania:matrix.org Add - + Dodaj Invite - + Zaproล› Cancel - Anuluj + Anuluj + + + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + ID pokoju lub alias + + + + LeaveRoomDialog + + + Leave room + Opuล›ฤ‡ pokรณj + + + + Are you sure you want to leave? + Czy na pewno chcesz wyjล›ฤ‡? @@ -724,7 +849,7 @@ Jeลผeli Nheko nie odnajdzie Twojego serwera domowego, wyล›wietli formularz umoลผ Your password. - + Twoje hasล‚o. @@ -739,18 +864,19 @@ Jeลผeli Nheko nie odnajdzie Twojego serwera domowego, wyล›wietli formularz umoลผ Homeserver address - + Adres Homeserwer-a server.my:8787 - + server.my:8787 The address that can be used to contact you homeservers client API. Example: https://server.my:8787 - + Adres ktรณry moลผe byฤ‡ uลผyty do komunikacji z klienckim API homeserwer-a. +Przykล‚ad: https://server.my:8787 @@ -758,25 +884,25 @@ Example: https://server.my:8787 ZALOGUJ - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Wprowadzono nieprawidล‚owe Matrix ID. Przykล‚ad prawidล‚owego ID: @ania:matrix.org - + Autodiscovery failed. Received malformed response. Automatyczne odkrywanie zakoล„czone niepowodzeniem. Otrzymano nieprawidล‚owฤ… odpowiedลบ. - + Autodiscovery failed. Unknown error when requesting .well-known. Automatyczne odkrywanie zakoล„czone niepowodzeniem. Napotkano nieznany bล‚ฤ…d. .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Nie odnaleziono wymaganych punktรณw koล„cowych. To moลผe nie byฤ‡ serwer Matriksa. @@ -786,33 +912,51 @@ Example: https://server.my:8787 Otrzymano nieprawidล‚owฤ… odpowiedลบ. Upewnij siฤ™, ลผe domena serwera domowego jest prawidล‚owa. - + An unknown error occured. Make sure the homeserver domain is valid. Wystฤ…piล‚ nieznany bล‚ฤ…d. Upewnij siฤ™, ลผe domena serwera domowego jest prawidล‚owa. - + SSO LOGIN Logowanie SSO - + Empty password Puste hasล‚o - + SSO login failed Logowanie SSO zakoล„czone niepowodzeniem + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed - + usuniฤ™to @@ -820,7 +964,7 @@ Example: https://server.my:8787 Szyfrowanie wล‚ฤ…czone - + room name changed to: %1 Nazwa pokoju zmieniona na: %1 @@ -842,7 +986,7 @@ Example: https://server.my:8787 %1 changed the room avatar - + %1 zmieniล‚ avatar pokoju @@ -857,7 +1001,7 @@ Example: https://server.my:8787 %1 placed a video call. - %1 rozpoczฤ…ล‚(-ฤ™ล‚a) poล‚ฤ…czenie wideo + %1 rozpoczฤ…ล‚(-ฤ™ล‚a) poล‚ฤ…czenie wideo. @@ -879,141 +1023,153 @@ Example: https://server.my:8787 Negotiating call... Negocjowanie poล‚ฤ…czeniaโ€ฆ + + + Allow them in + Wpuล›ฤ‡ + MessageInput Hang up - + Rozล‚ฤ…cz siฤ™ Place a call - + Zadzwoล„ Send a file - Wyล›lij plik + Wyล›lij plik Write a message... - Napisz wiadomoล›ฤ‡โ€ฆ + Napisz wiadomoล›ฤ‡โ€ฆ - + Stickers - + Naklejki Emoji - Emoji + Emoji Send - + Wyล›lij You don't have permission to send messages in this room - + Nie masz uprawnieล„ do wysyล‚ania wiadomoล›ci w tym pokoju MessageView - + Edit - + Edytuj React - + Zareaguj Reply - + Odpisz Options - + Opcje - + + &Copy - + &Kopiuj - + + Copy &link location - + Kopiuj &adres odnoล›nika - + Re&act - + Zar&eaguj Repl&y - + Odp&isz &Edit - + &Edytuj Read receip&ts - + Sprawdลบ &odbiorcรณw &Forward - + &Przekaลผ dalej &Mark as read - + &Oznacz jako przeczytane View raw message - + Wyล›wietl nowe wiadomoล›ci View decrypted raw message - + Wyล›wietl odszyfrowanฤ… nowฤ… wiadomoล›ฤ‡ Remo&ve message - + &Usuล„ wiadomoล›ฤ‡ &Save as - + &Zapisz jako &Open in external program - + Otwรณrz w &zewnฤ™trznym programie Copy link to eve&nt - + Skopiuj link do z&darzenia + + + + &Go to quoted message + Idลบ do zacytowanej wiado&moล›ci @@ -1021,37 +1177,42 @@ Example: https://server.my:8787 Send Verification Request - + Wyล›lij proล›bฤ™ o weryfikacjฤ™ Received Verification Request + Otrzymano proล›bฤ™ o weryfikacjฤ™ + + + + 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.) - + 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? - + Aby umoล›liwiฤ‡ innym uลผytkownikom identyfikacjฤ™, ktรณre urzฤ…dzenia faktycznie naleลผฤ… do Ciebie, moลผesz wykonaฤ‡ ich weryfikacjฤ™. To rรณwnieลผ umoลผliwi automatyczny backup kluczy. Zweryfikowaฤ‡ %1 teraz? To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party. - + Aby upewniฤ‡ siฤ™, ลผe nikt nie podsล‚uchuje twojej komunikacji moลผesz wykonaฤ‡ proces weryfikacji rozmรณwcy. %1 has requested to verify their device %2. - + %1 poprosiล‚ Ciฤ™ o weryfikacjฤ™ jego/jej urzฤ…dzenia: %2. %1 using the device %2 has requested to be verified. - + Uลผytkownik %1 poprosiล‚ Ciฤ™ o weryfikacjฤ™ swojego urzฤ…dzenia: %2. Your device (%1) has requested to be verified. - + Twoje urzฤ…dzenie (%1) poprosiล‚o o weryfikacjฤ™. @@ -1074,48 +1235,45 @@ Example: https://server.my:8787 Akceptuj + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message - + %1 wysล‚aล‚(a) zaszyfrowanฤ… wiadomoล›ฤ‡ - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - + Format wiadomoล›ci w powiadomieniu. %1 to nadawca, %2 to wiadomoล›ฤ‡. + %1 odpisaล‚(a): %2 %1 replied with an encrypted message - + %1 odpisaล‚(a) zaszyfrowanฤ… wiadomoล›ciฤ… %1 replied to a message - + %1 odpisaล‚(a) na wiadomoล›ฤ‡ %1 sent a message - + %1 wysล‚aล‚(a) wiadomoล›ฤ‡ @@ -1123,32 +1281,32 @@ Example: https://server.my:8787 Place a call to %1? - + Rozpoczฤ…ฤ‡ poล‚ฤ…czenie gล‚osowe z %1? No microphone found. - + Nie znaleziono mikrofonu. - + Voice - + Dลบwiฤ™k Video - + Wideo Screen - + Ekran Cancel - Anuluj + Anuluj @@ -1162,7 +1320,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. Stwรณrz unikalny profil, ktรณry pozwoli Ci na zalogowanie siฤ™ do kilku kont jednoczeล›nie i uruchomienie wielu instancji Nheko. @@ -1177,21 +1335,37 @@ Example: https://server.my:8787 nazwa profilu + + ReadReceipts + + + Read receipts + Potwierdzenia przeczytania + + + + ReadReceiptsModel + + + Yesterday, %1 + Wczoraj, %1 + + RegisterPage - + Username Nazwa uลผytkownika - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. Nazwa uลผytkownika nie moลผe byฤ‡ pusta i moลผe zawieraฤ‡ wyล‚ฤ…cznie znaki a-z, 0-9, ., _, =, -, i /. - + Password Hasล‚o @@ -1211,7 +1385,7 @@ Example: https://server.my:8787 Serwer domowy - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. Serwer, ktรณry pozwala na rejestracjฤ™. Poniewaลผ Matrix jest zdecentralizowany, musisz najpierw znaleลบฤ‡ serwer ktรณry pozwala na rejestracjฤ™ bฤ…dลบ hostowaฤ‡ swรณj wล‚asny. @@ -1221,52 +1395,42 @@ Example: https://server.my:8787 ZAREJESTRUJ - - No supported registration flows! - Nie wspierana procedura rejestracji! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - Automatyczne odkrywanie zakoล„czone niepowodzeniem. Otrzymano nieprawidล‚owฤ… odpowiedลบ. + Automatyczne odkrywanie zakoล„czone niepowodzeniem. Otrzymano nieprawidล‚owฤ… odpowiedลบ. - + Autodiscovery failed. Unknown error when requesting .well-known. - Automatyczne odkrywanie zakoล„czone niepowodzeniem. Napotkano nieznany bล‚ฤ…d. .well-known. + Automatyczne odkrywanie zakoล„czone niepowodzeniem. Napotkano nieznany bล‚ฤ…d. .well-known. - + The required endpoints were not found. Possibly not a Matrix server. - Nie odnaleziono wymaganych punktรณw koล„cowych. To moลผe nie byฤ‡ serwer Matriksa. + Nie odnaleziono wymaganych interfejsรณw. To moลผe nie byฤ‡ serwer Matrix. Received malformed response. Make sure the homeserver domain is valid. - Otrzymano nieprawidล‚owฤ… odpowiedลบ. Upewnij siฤ™, ลผe domena serwera domowego jest prawidล‚owa. + Otrzymano nieprawidล‚owฤ… odpowiedลบ. Upewnij siฤ™, ลผe domena homeserver-a jest prawidล‚owa. An unknown error occured. Make sure the homeserver domain is valid. - Wystฤ…piล‚ nieznany bล‚ฤ…d. Upewnij siฤ™, ลผe domena serwera domowego jest prawidล‚owa. + Wystฤ…piล‚ nieznany bล‚ฤ…d. Upewnij siฤ™, ลผe domena homeserver-a jest prawidล‚owa. - + Password is not long enough (min 8 chars) Hasล‚o jest zbyt krรณtkie (min. 8 znakรณw) - + Passwords don't match Hasล‚a nie pasujฤ… do siebie - + Invalid server name Nieprawidล‚owa nazwa serwera @@ -1274,295 +1438,390 @@ Example: https://server.my:8787 ReplyPopup - + Close Zamknij Cancel edit + Anuluj edytowanie + + + + RoomDirectory + + + Explore Public Rooms + Przeglฤ…daj Pokoje Publiczne + + + + Search for public rooms + Szukaj publicznych pokojรณw + + + + Choose custom homeserver RoomInfo - + no version stored - + wersja nie zostaล‚a zachowana RoomList - + New tag - + Nowy tag Enter the tag you want to use: - - - - - Leave Room - - - - - Are you sure you want to leave this room? - + Wprowadลบ tag, ktรณrego chcesz uลผyฤ‡: Leave room - Opuล›ฤ‡ pokรณj + Opuล›ฤ‡ pokรณj Tag room as: - + Oznacz (tag) pokรณj jako: Favourite - + Ulubione Low priority - + Niski priorytet Server notice - + Powiadomienie serwera Create new tag... - + Utwรณrz nowy tag... - + Status Message - + Wiadomoล›ฤ‡ Statusowa Enter your status message: - + Wprowadลบ swojฤ… wiadomoล›ฤ‡ statusowฤ…: Profile settings - + Ustawienia profilu Set status message + Ustaw wiadomoล›ฤ‡ statusowฤ… + + + + Logout + Wyloguj + + + + Encryption not set up + Cross-signing setup has not run yet. - - Logout - Wyloguj + + Unverified login + The user just signed in with this device and hasn't verified their master key. + - + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Zamknij + + + Start a new chat - Utwรณrz nowy czat + Utwรณrz nowy czat Join a room - Doล‚ฤ…cz do pokoju + Doล‚ฤ…cz do pokoju Create a new room - + Utwรณrz nowy pokรณj Room directory - Katalog pokojรณw + Katalog pokojรณw - + User settings - Ustawienia uลผytkownika + Ustawienia uลผytkownika RoomMembers - + Members of %1 - + Obecni w %1 - + %n people in %1 Summary above list of members - - - - + + %n osoba w %1 + %n osรณb w %1 + %n osรณb w %1 Invite more people - + Zaproล› wiฤ™cej ludzi + + + + This room is not encrypted! + Ten pokรณj jest szyfrowany! + + + + This user is verified. + Ten uลผytkownik zostaล‚ zweryfikowany. + + + + This user isn't verified, but is still using the same master key from the first time you met. + Ten uลผytkownik nie zostaล‚ zweryfikowany, ale wciฤ…ลผ uลผywa tego samego klucza gล‚รณwnego ktรณrego uลผywaล‚ podczas waszej pierwszej rozmowy. + + + + This user has unverified devices! + Ten uลผytkownik ma urzฤ…dzenie, ktรณre nie zostaล‚y zweryfikowane! RoomSettings - + Room Settings - + Ustawienia Pokoju - + %1 member(s) - + %1 uลผytkownik(รณw) SETTINGS - + USTAWIENIA Notifications - + Powiadomienia Muted - + Wyciszony Mentions only - + Tylko wzmianki All messages - + Wszystkie wiadomoล›ci - + + Room access + Dostฤ™p do pokoju + + + Anyone and guests - + Kaลผdy oraz goล›cie Anyone - + Kaลผdy Invited users - + Zaproszeni uลผytkownicy - + + By knocking + Pukajฤ…c + + + + Restricted by membership in other rooms + Zastrzeลผone poprzez czล‚onkostwo w innych pokojach + + + Encryption - + Szyfrowanie End-to-End Encryption - Szyfrowanie end-to-end + Szyfrowanie end-to-end Encryption is currently experimental and things might break unexpectedly. <br> Please take note that it can't be disabled afterwards. - + Szyfrowanie w chwili obecnej jest eksperymentalne i moลผe popsuฤ‡ siฤ™ w dowolnym momencie.<br> + Proszฤ™ pamiฤ™taj, ลผe szyfrowanie nie bฤ™dzie mogล‚o zostaฤ‡ pรณลบniej wyล‚ฤ…czone. Sticker & Emote Settings - + Naklejki i Ustawienia Emote Change - + Zmieล„ Change what packs are enabled, remove packs or create new ones - + Wybieลผ, ktรณre paczki sฤ… wล‚ฤ…czone, usuล„ paczki, lub utwรณrz nowe INFO - + INFO Internal ID - + Wewnฤ™trzne ID Room Version - + Wersja Pokoju - + Failed to enable encryption: %1 - Nie udaล‚o siฤ™ wล‚ฤ…czyฤ‡ szyfrowania: %1 + Nie udaล‚o siฤ™ wล‚ฤ…czyฤ‡ szyfrowania: %1 - + Select an avatar - Wybierz awatar + Wybierz awatar All Files (*) - Wszystkie pliki (*) + Wszystkie pliki (*) The selected file is not an image - + Wybrany plik nie jest obrazem Error while reading file: %1 - + Bล‚ฤ…d czytania pliku: %1 - - + + Failed to upload image: %s - Nie udaล‚o siฤ™ wysล‚aฤ‡ obrazu: %s + Nie udaล‚o siฤ™ wysล‚aฤ‡ obrazu: %s RoomlistModel - + Pending invite. - + Oczekujฤ…ce zaproszenie. - + Previewing this room + Podglฤ…d tego pokoju + + + + No preview available + Podglฤ…d pokoju niedostฤ™pny + + + + Root + + + Please enter your login password to continue: - - No preview available + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. @@ -1571,53 +1830,168 @@ Example: https://server.my:8787 Share desktop with %1? - + Udostฤ™pniฤ‡ pulpit (desktop) uลผytkownikowi: %1? Window: - + Okno: Frame rate: - + Klatek na sekundฤ™: Include your camera picture-in-picture - + Wล‚ฤ…cz funkcjฤ™ picture-in-picture kamery Request remote camera - + Poproล› rozmรณwcฤ™ o wล‚ฤ…czenie kamery View your callee's camera like a regular video call - + Wyล›wietl widok kamery rozmรณwcy jak podczas zwykล‚ej rozmoowy wideo Hide mouse cursor - + Ukryj kursor myszy Share - + Udostฤ™pnij Preview - + Podglฤ…d Cancel - Anuluj + Anuluj + + + + SecretStorage + + + Failed to connect to secret storage + Bล‚ฤ…d poล‚ฤ…czenia do menadลผera sekretรณw + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Nie udaล‚o siฤ™ uaktualniฤ‡ paczki obrazรณw: %1 + + + + Failed to delete old image pack: %1 + Nie udaล‚o siฤ™ usunฤ…ฤ‡ starej paczki obrazรณw: %1 + + + + Failed to open image: %1 + Nie udaล‚o siฤ™ otworzyฤ‡ obrazu: %1 + + + + Failed to upload image: %1 + Nie udaล‚o siฤ™ wysล‚aฤ‡ obrazu: %1 @@ -1625,22 +1999,22 @@ Example: https://server.my:8787 Failed - + Bล‚ฤ…d Sent - + Wysล‚ano Received - + Otrzymano Read - + Przeczytano @@ -1648,7 +2022,7 @@ Example: https://server.my:8787 Search - Szukaj + Szukaj @@ -1656,274 +2030,306 @@ Example: https://server.my:8787 Successful Verification - + Weryfikacja udana Verification successful! Both sides verified their devices! - + Weryfikacja udana! Obaj rozmรณwcy zweryfikowali swoje urzฤ…dzenia! Close - Zamknij + Zamknij TimelineModel - + Message redaction failed: %1 - Redagowanie wiadomoล›ci nie powiodล‚o siฤ™: %1 + Cenzurowanie wiadomoล›ci nie powiodล‚o siฤ™: %1 - + Failed to encrypt event, sending aborted! - + Szyfrowanie event-u nie powiodล‚o siฤ™, wysyล‚anie anulowane! - + Save image - Zapisz obraz + Zapisz obraz Save video - + Zapisz wideo Save audio - + Zapisz audio Save file - + Zapisz plik - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) - - - - + + %1 %2 pisze. + %1, oraz %2 piszฤ… (w sumie %n osรณb). + %1, oraz %2 piszฤ… (w sumie %n osรณb). - + %1 opened the room to the public. - + %1 zmieniล‚(a) status pokoju na publiczny. %1 made this room require and invitation to join. - + %1 oznaczyล‚(a) pokรณj jako wymagajฤ…cy zaproszenia aby do niego doล‚ฤ…czyฤ‡. - + + %1 allowed to join this room by knocking. + %1 zazwoliล‚(a) na doล‚ฤ…czenie do tego pokoju poprzez pukanie. + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 zezwoliล‚(a) czล‚onkom nastฤ™pujฤ…cych pokojรณw na automatyczne doล‚ฤ…czenie do tego pokoju: %2 + + + %1 made the room open to guests. - + %1 pozwoliล‚a na dostฤ™p goล›ciom do tego pokoju. %1 has closed the room to guest access. - + %1 zabroniล‚(a) goล›ciom dostฤ™pu do tego pokoju. %1 made the room history world readable. Events may be now read by non-joined people. - + %1 zezwoliล‚a kaลผdemu na dostฤ™p do historii tego pokoju. Eventy mogฤ… teraz zostaฤ‡ przeczytane przez osoby nie bฤ™dฤ…ce czล‚onkami tego pokoju. %1 set the room history visible to members from this point on. - + %1 umoลผliwiล‚ czล‚onkom tego pokoju na dostฤ™p od teraz. - + %1 set the room history visible to members since they were invited. - + %1 umoลผliwiล‚ dostฤ™p do historii tego pokoju czล‚onkom od momentu kiedy zostali zaproszeni. - + %1 set the room history visible to members since they joined the room. - + %1 umoลผliwiล‚ dostฤ™p do historii tego pokoju czล‚onkom od momentu kiedy doล‚ฤ…czyli do pokoju. %1 has changed the room's permissions. - + %1 zmieniล‚(a) uprawnienia pokoju. - + %1 was invited. - + %1 zostaล‚(a) zaproszona/y. - + %1 changed their avatar. - + %1 zmieniล‚(a) swรณj awatar. %1 changed some profile info. - + %1 zmodyfikowaล‚(a) dane profilu. - + %1 joined. - + %1 doล‚ฤ…czyล‚(a). - + + %1 joined via authorisation from %2's server. + %1 doล‚ฤ…czyล‚a dziฤ™ki autoryzacji serwera uลผytkownika %2. + + + %1 rejected their invite. - + %1 odrzuciล‚(a) zaproszenie. Revoked the invite to %1. - + Uniewaลผniono zaproszenie dla %1. %1 left the room. - + %1 opuล›ciล‚(a) pokรณj. Kicked %1. - + Wykopano uลผytkownika %1. Unbanned %1. - + Odbanowano uลผytkownika %1. %1 was banned. - + Uลผytkownik %1 zostaล‚ zbanowany. - + Reason: %1 - + Powรณd: %1 - + %1 redacted their knock. - + Uลผytkownik %1 ocenzurowaล‚ wล‚asne pukanie. - + You joined this room. - Doล‚ฤ…czyล‚eล›(-ล‚aล›) do tego pokoju. + Doล‚ฤ…czyล‚eล›(-ล‚aล›) do tego pokoju. - + %1 has changed their avatar and changed their display name to %2. - + Uลผytkownik %1 zmieniล‚ swojego awatara i zmieniล‚ swojฤ… nazwฤ™ wyล›wietlanฤ… na %2. - + %1 has changed their display name to %2. - + Uลผytkownik %1 zmieniล‚ swojฤ… nazwฤ™ wyล›wietlanฤ… na %2. - + Rejected the knock from %1. - + Odrzucono pukanie uลผytkownika %1. %1 left after having already left! This is a leave event after the user already left and shouldn't happen apart from state resets - + %1 opuล›ciล‚(a) pokรณj po raz kolejny! %1 knocked. - + %1 zapukaล‚(a). TimelineRow - + Edited - + Edytowane TimelineView - + No room open - + Brak otwartych pokojรณw - + + No preview available + Podglฤ…d pokoju niedostฤ™pny + + + %1 member(s) - + %1 obeczny(ch) join the conversation - + Doล‚ฤ…cz do rozmowy accept invite - + zaakceptรณj zaproszenie decline invite - + odrzuฤ‡ zaproszenie Back to room list - - - - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - + Wrรณc do listy pokoi TopBar - + Back to room list - + Wrรณฤ‡ do listy pokoi - + No room selected + Nie wybrano pokoju + + + + This room is not encrypted! + Ten pokรณj nie jest szyfrowany! + + + + This room contains only verified devices. + Ten pokรณj zawiera wyล‚ฤ…cznie zweryfikowane urzฤ…dzenia. + + + + This room contains verified devices and devices which have never changed their master key. - + + This room contains unverified devices! + Ten pokรณj zawiera niezweryfikowane urzฤ…dzenia! + + + Room options - Ustawienia pokoju + Ustawienia pokoju Invite users - Zaproล› uลผytkownikรณw + Zaproล› uลผytkownikรณw Members - Czล‚onkowie + Czล‚onkowie @@ -1949,78 +2355,168 @@ Example: https://server.my:8787 Zakoล„cz + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Proszฤ™ wprowadziฤ‡ prawidล‚owy token rejestracji. + + + + Invalid token + + + UserProfile - + Global User Profile - + Globalny Profil Uลผytkownika Room User Profile - + Profil Uลผytkownika Pokoju - - + + Change avatar globally. + Zmieล„ awatar globalnie. + + + + Change avatar. Will only apply to this room. + Zmieล„ awatar wyล‚ฤ…cznie dla bieลผฤ…cego pokoju. + + + + Change display name globally. + Zmieล„ nazwฤ™ wyล›wietlanฤ… globalnie. + + + + Change display name. Will only apply to this room. + Zmieล„ nazwฤ™ wyล›wietlanฤ… wyล‚ฤ…cznie dla bieลผฤ…cego pokoju. + + + + Room: %1 + Pokรณj: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + To profil specyficzny dla pokoju. Nazwa uลผytkownika oraz awatar mogฤ… byฤ‡ inne niลผ globalna nazwa uลผytkownika i globalny awatar. + + + + Open the global profile for this user. + Otwรณrz globalny profil tego uลผytkownika. + + + + Verify - + Zweryfikuj - - Ban the user - - - - - Start a private chat - + + Start a private chat. + Rozpocznij prywatny czat. - Kick the user + Kick the user. + Wykop uลผytkownika. + + + + Ban the user. + Zbanuj uลผytkownika. + + + + Refresh device list. - + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify + Udweryfikuj + + + + Sign out device %1 - + + You signed out this device. + + + + Select an avatar - Wybierz awatar + Wybierz awatar All Files (*) - Wszystkie pliki (*) + Wszystkie pliki (*) The selected file is not an image - + Wybrany plik nie jest obrazem Error while reading file: %1 - + Bล‚ฤ…d odczytu pliku: %1 UserSettings - - + + Default - + Domyล›lne UserSettingsPage - + Minimize to tray Zminimalizuj do paska zadaล„ @@ -2030,29 +2526,29 @@ Example: https://server.my:8787 Rozpocznij na pasku zadaล„ - + Group's sidebar Pasek boczny grupy - + Circular Avatars - + Okrฤ…gล‚e awatary - + profile: %1 - + profil: %1 - + Default - + Domyล›lny CALLS - + POลฤ„CZENIA @@ -2062,87 +2558,101 @@ Example: https://server.my:8787 REQUEST - + POPROลš O DOWNLOAD - + POBIERZ - + Keep the application running in the background after closing the client window. - + Pozostaw aplikacjฤ™ dziaล‚ajฤ…cฤ… w tle po zamkniฤ™ciu okna. Start the application in the background without showing the client window. - + Uruchamiaj aplikacjฤ™ w tle bez wyล›wietlania okna gล‚รณwnego. Change the appearance of user avatars in chats. OFF - square, ON - Circle. + Zmieล„ wyglฤ…d awatarรณw w czasie. +OFF - kwadrat, ON - koล‚o. + + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. Show a column containing groups and tags next to the room list. - + Pokazuj kolumnฤ™ zawierajฤ…cฤ… grupy i tagi obok listy pokoi. Decrypt messages in sidebar - + Odszyfruj wiadomoล›ci na pasku bocznym Decrypt the messages shown in the sidebar. Only affects messages in encrypted chats. - + Odszyfruj wiadomoล›ci na pasku bocznym. +Dotyczy wyล‚ฤ…cznie czatรณw z wล‚ฤ…czonym szyfrowaniem. Privacy Screen - + Ekran prywatnoล›ci When the window loses focus, the timeline will be blurred. - + Kiedy okno traci fokus, historia zostanie rozmyta. - + Privacy screen timeout (in seconds [0 - 3600]) - + Opรณลบnienie ekranu prywatnoล›ci (w sekundach [0 - 3600]) Set timeout (in seconds) for how long after window loses focus before the screen will be blurred. Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) - + Ustaw czas (w sekundach) po ktรณrym okno zostanie rozmyte po +stracie fokusu. +Ustaw na 3 aby rozmywaฤ‡ natychmiast po stracie fokusu. Maksymalna wartoล›ฤ‡ to 1 godz (3600 sekund) Show buttons in timeline - + Pokazuj przyciski w historii Show buttons to quickly reply, react or access additional options next to each message. - + Pokazuj przyciski do reakcji albo dostฤ™pu do dodatkowych opcji obok kaลผdej wiadomoล›ci. Limit width of timeline - + Ogranicz szerokoล›ฤ‡ historii Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised - + Ustaw maksymalnฤ… szerokoล›ฤ‡ wiadomoล›ci w historii (w pikselach). Moลผe to poprawiฤ‡ czytelnoล›ฤ‡ gdy Nheko zostanie zmaksymalizowany na szerokim ekranie @@ -2153,22 +2663,24 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds Show who is typing in a room. This will also enable or disable sending typing notifications to others. - + To rรณwnieลผ wล‚ฤ…czy lub wyล‚ฤ…czy wysyล‚anie powiadomieล„ o pisaniu do innych. Sort rooms by unreads - + Sortuj pokoje po nieprzeczytanych wiadomoล›ciach Display rooms with new messages first. If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room. If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. - + Wyล›wietlaj wiadomoล›ci z nieprzeczytanymi wiadomoล›ciami w pierwszej kolejnoล›ci. +Gdy ta opcja jest wyล‚ฤ…czona, pokoje bฤ™dฤ… sortowane wyล‚ฤ…cznie po znaczniku czasowym ostatniej wiadomoล›ci w pokoju. +Gdy ta opcja jest wล‚ฤ…czona, pokoje z aktywnymi powiadomieniami (maล‚o kรณล‚ko z numerkiem w ล›rodku) bฤ™dฤ… na poczฤ…tku. Pokoje, ktรณre sฤ… wyciszone, bฤ™dฤ… sortowane po znaczniku czasowym, poniewaลผ zdaje siฤ™, ลผe nie uwaลผasz ich za rรณwnie wazne co pozostaล‚e pokoje. - + Read receipts Potwierdzenia przeczytania @@ -2176,94 +2688,137 @@ If this is on, rooms which have active notifications (the small circle with a nu Show if your message was read. Status is displayed next to timestamps. - + Pokaลผ czy twoja wiadomoล›ฤ‡ zostaล‚a przeczytana. +Status jest wyล›wietlany obok znacznika czasu. - + Send messages as Markdown - + Wysyล‚aj wiadomoล›ci uลผywajฤ…c Markdown-u Allow using markdown in messages. When disabled, all messages are sent as a plain text. - + Pozwรณl na uลผywanie markdown-u w wiadomoล›ciach. +Gdy ta opcja jest wyล‚ฤ…czona, wszystkie wiadomoล›ci bฤ™dฤ… wysyล‚ane goล‚ym tekstem. + Play animated images only on hover + Odtwarzaj animacje obrazรณw tylko gdy kursor myszy jest nad nimi + + + Desktop notifications Powiadomienia na pulpicie Notify about received message when the client is not currently focused. - + Powiadamiaj o odrzymanych wiadomoล›ciach gdy klient nie jest obecnie zfokusowany. Alert on notification - + Alert podczas notyfikacji Show an alert when a message is received. This usually causes the application icon in the task bar to animate in some fashion. - + Pokazuj alert gdy przychodzi wiadomoล›ฤ‡. +To zwykle sprawia, ลผe ikona aplikacji w tacce systemowej jest animowana. Highlight message on hover - + Wyrรณลผnij wiadomoล›ci gdy kursor myszy znajduje siฤ™ nad nimi. Change the background color of messages when you hover over them. - + Zmieล„ tล‚o wiadomoล›ci kiedy kursor myszy znajduje siฤ™ nad nimi. Large Emoji in timeline - + Duลผe emotikony w historii Make font size larger if messages with only a few emojis are displayed. - + Zwiฤ™ksz rozmiar czcionki gdy wiadomoล›ci zawierajฤ… tylko kilka emotikon. - + + Send encrypted messages to verified users only + Wysyล‚aj zaszyfrowane wiadomoล›ci wyล‚ฤ…cznie do zweryfikowanych uลผytkownikรณw + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + Wymaga zweryfikowania uลผytkownika zanim bฤ™dzie moลผliwe wysล‚anie zaszyfrowanych wiadomoล›ci do niego. To zwiฤ™ksza bezpieczeล„stwo, ale sprawia, ลผe szyfrowanie E2E jest bardziej niewygodne w uลผyciu. + + + Share keys with verified users and devices - + Udostฤ™pnij klucze zweryfikowanym uลผytkownikom i urzฤ…dzeniom - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + Automatycznie odpowiada na proล›by o klucze od zweryfikowanych uลผytkownikรณw, nawet gdy ci nie powinni mieฤ‡ dostฤ™pu do tych kluczy. + + + + Online Key Backup + Backup Kluczy Online + + + + Download message encryption keys from and upload to the encrypted online key backup. + Pobierz klucze szyfrowania wiadomoล›ci i umieล›ฤ‡ w szyfrowanym backup-ie kluczy online. + + + + Enable online key backup + Wล‚ฤ…cz backup kluczy online + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + Autorzy Nheko nie zalecajฤ… wล‚ฤ…czenia backup-u kluczy online dopรณki symetryczny backup kluczy online nie jest dostฤ™pny. Wล‚ฤ…czyฤ‡ mimo to? + + + CACHED - + ZAPISANE W CACHE-U NOT CACHED - + NIE ZAPISANE W CACHE-U - + Scale factor - + Wspรณล‚czynnik skalowania Change the scale factor of the whole user interface. - + Zmieล„ wspรณล‚czynnik skalowania caล‚ego interfejsu uลผytkownika. Font size - + Wielkoล›ฤ‡ czcionki Font Family - + Rodzina czcionki @@ -2273,42 +2828,42 @@ This usually causes the application icon in the task bar to animate in some fash Ringtone - + Dzwonek Set the notification sound to play when a call invite arrives - + Ustaw dลบwiฤ™k powiadomienia odtwarzanego podczas zaproszenia do poล‚ฤ…czenia Microphone - + Mikrofon Camera - + Kamera Camera resolution - + Rozdzielczoล›ฤ‡ kamery Camera frame rate - + Iloล›ฤ‡ klatek na sekundฤ™ kamery Allow fallback call assist server - + Pozwรณl na korzystanie z serwera pomocniczego do nawiฤ…zywania poล‚ฤ…czeล„ gล‚osowych/wideo Will use turn.matrix.org as assist when your home server does not offer one. - + Uลผywaj serwera pomocniczego turn.matrix.org gdy twรณj homeserver nie udostฤ™pnia wล‚asnego serwera pomocniczego. @@ -2321,19 +2876,19 @@ This usually causes the application icon in the task bar to animate in some fash Odcisk palca urzฤ…dzenia - + Session Keys - + Klucze sesji IMPORT - + IMPORTUJ EXPORT - + EKSPORTUS @@ -2341,126 +2896,134 @@ This usually causes the application icon in the task bar to animate in some fash SZYFROWANIE - + GENERAL OGร“LNE - + INTERFACE - + INTERFEJS - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + Odtwarzaj media takie jak GIF czy WEBP tylko gdy wskazane przy uลผyciu myszy. + + + Touchscreen mode - + Tryb ekranu dotykowego Will prevent text selection in the timeline to make touch scrolling easier. - + Zapobiega zaznaczaniu tekstu w historii rozmรณw by uล‚atwiฤ‡ przewijanie przy uลผyciu interfejsu dotykowego. Emoji Font Family - + Rodzina czcionki emotikon - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key - + Gล‚รณwny klucz podpisywania Your most important key. You don't need to have it cached, since not caching it makes it less likely it can be stolen and it is only needed to rotate your other signing keys. - + Twรณj najwaลผniejszy klucz. Nie musi byฤ‡ zapisany w cache-u -- nie zapisujฤ…c go w cache-u zmniejszasz ryzyko, ลผe ten klucz zostanie wykradzony. Ponadto, ten klucz jest potrzebny wyล‚ฤ…cznie podczas odล›wierzania kluczy podpisywania sesji. User signing key - + Klucz podpisywania uลผytkownika The key to verify other users. If it is cached, verifying a user will verify all their devices. - + Klucz uลผywany do weryfikacji innych uลผytkownikรณw. Gdy zapisany w cache-u, weryfikacja uลผytkownika dokona weryfikacji wszystkich jego urzฤ…dzeล„. - + Self signing key - + Klucz samo-podpisywania The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you. - + Klucz sล‚uลผฤ…cy do weryfikacji twoich wล‚asnych urzฤ…dzeล„. Jeล›li zapisany w cache-u, weryfikacja jednego z twoich urzฤ…dzeล„ sprawi, ลผe bฤ™dzie ono zweryfikowane dla wszystkich twoich pozostaล‚ych urzฤ…dzeล„ oraz dla tych uลผytkownikรณw, ktรณrzy zweryfikowali Ciebie. Backup key - + Klucz backup-u The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server. - + Klucz sล‚uลผฤ…cy do odszyfrowania backup-u kluczy online. Jeล›li zapisany w cache-u, moลผesz wล‚ฤ…czyฤ‡ backup kluczy online by zapisaฤ‡ w klucze zaszyfrowane bezpiecznie w backup-ie na serwerze. Select a file - Wybierz plik + Wybierz plik All Files (*) - Wszystkie pliki (*) + Wszystkie pliki (*) - + Open Sessions File - + Otwรณrz Plik Sesji - + Error - + Bล‚ฤ…d - - + + File Password - + Hasล‚o Pliku - + Enter the passphrase to decrypt the file: - + Wpisz frazฤ™ do odszyfrowania pliku: - + The password cannot be empty - + Hasล‚o nie moลผe byฤ‡ puste Enter passphrase to encrypt your session keys: - + Wpisz frazฤ™ do odszyfrowania twoich kluczy sesji: File to save the exported session keys - + Plik do ktรณrego zostanฤ… wyeksportowane klucze sesji + + + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Nie znaleziono zaszyfrowanego prywatnego czatu z tym uลผytkownikiem. Utwรณrz nowy zaszyfrowany prywatny czat z tym uลผytkownikiem i sprรณbuj ponownie. @@ -2468,22 +3031,22 @@ This usually causes the application icon in the task bar to animate in some fash Waiting for other partyโ€ฆ - + Oczekiwanie na rozmรณwcฤ™... Waiting for other side to accept the verification request. - + Oczekiwanie za zaakceptowanie proล›by o weryfikacjฤ™ przez rozmรณwcฤ™. Waiting for other side to continue the verification process. - + Oczekiwanie na kontynuowanie weryfikacji przez rozmรณwcฤ™. Waiting for other side to complete the verification process. - + Oczekiwanie na zakoล„czenie procesu weryfikacji przez rozmรณwcฤ™. @@ -2519,7 +3082,7 @@ This usually causes the application icon in the task bar to animate in some fash Yesterday - + Wczoraj @@ -2527,7 +3090,7 @@ This usually causes the application icon in the task bar to animate in some fash Create room - + Utwรณrz pokรณj @@ -2570,7 +3133,7 @@ This usually causes the application icon in the task bar to animate in some fash Open Fallback in Browser - + W razie koniecznoล›ci otwรณrz w przeglฤ…darce @@ -2580,43 +3143,12 @@ This usually causes the application icon in the task bar to animate in some fash Confirm - + Potwierdลบ Open the fallback, follow the steps and confirm after completing them. - - - - - dialogs::JoinRoom - - - Join - - - - - Cancel - Anuluj - - - - Room ID or alias - ID pokoju lub alias - - - - dialogs::LeaveRoom - - - Cancel - Anuluj - - - - Are you sure you want to leave? - Czy na pewno chcesz wyjล›ฤ‡? + Otwรณrz w przeglฤ…darce i postฤ™puj zgodnie z instrukcjฤ…. @@ -2664,7 +3196,7 @@ Rozmiar multimediรณw: %2 Confirm - + Potwierdลบ @@ -2672,151 +3204,125 @@ Rozmiar multimediรณw: %2 Rozwiฤ…ลผ reCAPTCHA i naciล›nij przycisk โ€žpotwierdลบโ€ - - dialogs::ReadReceipts - - - Read receipts - Potwierdzenia przeczytania - - - - Close - Zamknij - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: You sent an audio clip - + Wysล‚aล‚eล›(aล›) klip audio %1 sent an audio clip - + %1 wysล‚aล‚(a) klip audio - + You sent an image - + Wysล‚aล‚eล›(aล›) obraz - + %1 sent an image - + %1 wysล‚aล‚(a) obraz - + You sent a file - + Wysล‚aล‚eล›(aล›) plik - + %1 sent a file - + %1 wysล‚aล‚(a) plik - + You sent a video - + Wysล‚aล‚eล›(aล›) wideo - + %1 sent a video - + %1 wysล‚aล‚(a) wideo - + You sent a sticker - + Wysล‚aล‚eล›(aล›) naklejkฤ™ - + %1 sent a sticker - + %1 wysล‚aล‚(a) naklejkฤ™ - + You sent a notification - + Wysล‚aล‚eล›(aล›) powiadomienie %1 sent a notification - + %1 wysล‚aล‚(a) powiadomienie You: %1 - + Ty: %1 - + %1: %2 - + %1: %2 You sent an encrypted message - + Wysล‚aล‚eล›(aล›) zaszyfrowanฤ… wiadomoล›ฤ‡ %1 sent an encrypted message - + %1 wysล‚aล‚(a) zaszyfrowanฤ… wiadomoล›ฤ‡ You placed a call - + Wykonujesz telefon - + %1 placed a call - + %1 wykonuje telefon - + You answered a call - + Odebraล‚eล›(aล›) poล‚ฤ…czenie - + %1 answered a call - + %1 odebraล‚(a) poล‚ฤ…czenie - + You ended a call - + Zakoล„czyล‚eล›(aล›) poล‚ฤ…czenie - + %1 ended a call - + %1 zakoล„czyล‚(a) poล‚ฤ…czenie utils - + Unknown Message Type - + Nieznany Typ Wiadomoล›ci diff --git a/resources/langs/nheko_pt_BR.ts b/resources/langs/nheko_pt_BR.ts index b83585fb..a9851709 100644 --- a/resources/langs/nheko_pt_BR.ts +++ b/resources/langs/nheko_pt_BR.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Ligando... @@ -40,7 +40,7 @@ Awaiting Confirmation - + Aguardando Confirmaรงรฃo @@ -56,7 +56,7 @@ CallInvite - + Video Call Chamada de Vรญdeo @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Chamada de Vรญdeo @@ -117,7 +117,7 @@ CallManager - + Entire screen Tela Inteira @@ -125,49 +125,49 @@ ChatPage - + Failed to invite user: %1 Falha ao convidar usuรกrio: %1 - + Invited user: %1 Usuรกrio convidado: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Falha ao migrar cache para versรฃo atual. Isso pode ter diferentes razรตes. Por favor reporte o problema e tente usar uma versรฃo antiga no meio tempo. Alternativamente, vocรช pode tentar excluir o cache manualmente. - + Confirm join Confirmar entrada Do you really want to join %1? - + Deseja realmente entrar em %1? - + Room %1 created. Sala %1 criada. - + Confirm invite Confirmar convite - + Do you really want to invite %1 (%2)? - + Deseja realmente convidar %1 (%2)? - + Failed to invite %1 to %2: %3 Falha ao convidar %1 para %2: %3 @@ -179,10 +179,10 @@ Do you really want to kick %1 (%2)? - + Deseja realmente expulsar %1 (%2)? - + Kicked user: %1 Usuรกrio expulso: %1 @@ -194,10 +194,10 @@ Do you really want to ban %1 (%2)? - + Deseja realmente banir %1 (%2)? - + Failed to ban %1 in %2: %3 Falha ao banir %1 em %2: %3 @@ -214,10 +214,10 @@ Do you really want to unban %1 (%2)? - + Deseja realmente desbanir %1 (%2)? - + Failed to unban %1 in %2: %3 Falha ao desbanir %1 em %2: %3 @@ -227,75 +227,77 @@ Usuรกrio desbanido: %1 - + Do you really want to start a private chat with %1? - + Deseja realmente iniciar uma conversa privada com %1? - + Cache migration failed! Migraรงรฃo do cache falhou! Incompatible cache version - + Versรฃo de cache incompatรญvel The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache. - + O cache em seu disco รฉ mais recente do que essa versรฃo do Nheko suporta. Por favor atualize ou limpe o cache. - + Failed to restore OLM account. Please login again. Falha ao restaurar conta OLM. Por favor faรงa login novamente. + + Failed to restore save data. Please login again. Falha ao restaurar dados salvos. Por favor faรงa login novamente. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - + Falha ao configurar chaves de criptografia. Resposta do servidor: %1 %2. Por favor tente novamente mais tarde. - - + + Please try to login again: %1 - + Failed to join room: %1 - + Falha ao entrar na sala: %1 - + You joined the room - + Vocรช entrou na sala Failed to remove invite: %1 - + Falha ao remover o convite: %1 - + Room creation failed: %1 Failed to leave room: %1 - + Falha ao sair da sala: %1 - + Failed to kick %1 from %2: %3 - + Falha ao expulsar %1 de %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + Cancelar + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close @@ -645,7 +744,7 @@ InputBar - + Select a file @@ -655,7 +754,7 @@ - + Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ Cancelar + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + + + + + LeaveRoomDialog + + + Leave room + + + + + Are you sure you want to leave? + + + LoginPage @@ -756,25 +881,25 @@ Example: https://server.my:8787 - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -784,35 +909,53 @@ Example: https://server.my:8787 - + An unknown error occured. Make sure the homeserver domain is valid. - + SSO LOGIN - + Empty password - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + Encryption enabled - + room name changed to: %1 @@ -862,18 +1005,23 @@ Example: https://server.my:8787 - + + Allow them in + + + + %1 answered the call. - + removed - + %1 ended the call. @@ -901,7 +1049,7 @@ Example: https://server.my:8787 - + Stickers @@ -924,7 +1072,7 @@ Example: https://server.my:8787 MessageView - + Edit @@ -944,17 +1092,19 @@ Example: https://server.my:8787 - + + &Copy - + + Copy &link location - + Re&act @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1072,33 +1232,29 @@ Example: https://server.my:8787 Aceitar + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 Nenhum microfone encontrado. - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 - + Password is not long enough (min 8 chars) - + Passwords don't match - + Invalid server name @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + + + + Start a new chat @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 - + User settings @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1414,16 +1607,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1453,7 +1666,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1468,7 +1686,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1514,12 +1742,12 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 - + Select an avatar @@ -1539,8 +1767,8 @@ Example: https://server.my:8787 - - + + Failed to upload image: %s @@ -1548,21 +1776,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1617,6 +1873,121 @@ Example: https://server.my:8787 Cancelar + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1669,18 +2040,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 - + Failed to encrypt event, sending aborted! - + Save image @@ -1700,7 +2071,7 @@ Example: https://server.my:8787 - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1709,7 +2080,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1719,7 +2090,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1739,12 +2120,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1754,12 +2135,12 @@ Example: https://server.my:8787 - + %1 was invited. - + %1 changed their avatar. @@ -1769,12 +2150,17 @@ Example: https://server.my:8787 - + %1 joined. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. @@ -1804,32 +2190,32 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. - + You joined this room. Vocรช entrou nessa sala. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. @@ -1848,7 +2234,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1856,12 +2242,17 @@ Example: https://server.my:8787 TimelineView - + No room open - + + No preview available + + + + %1 member(s) @@ -1886,28 +2277,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options @@ -1945,10 +2348,35 @@ Example: https://server.my:8787 + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1958,33 +2386,98 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar @@ -2007,8 +2500,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2016,7 +2509,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2026,22 +2519,22 @@ Example: https://server.my:8787 - + Group's sidebar - + Circular Avatars - + profile: %1 - + Default @@ -2066,7 +2559,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2081,6 +2574,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2109,7 +2612,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts @@ -2175,7 +2678,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown @@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications @@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File - + @@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash - - + + File Password - + Enter the passphrase to decrypt the file: - + The password cannot be empty @@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - Cancelar - - - - Room ID or alias - - - - - dialogs::LeaveRoom - - - Cancel - Cancelar - - - - Are you sure you want to leave? - - - dialogs::Logout @@ -2666,32 +3186,6 @@ Media size: %2 - - dialogs::ReadReceipts - - - Read receipts - - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2705,47 +3199,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2760,7 +3254,7 @@ Media size: %2 - + %1: %2 @@ -2780,27 +3274,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2808,7 +3302,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/langs/nheko_pt_PT.ts b/resources/langs/nheko_pt_PT.ts index a0a8c8a8..69eab896 100644 --- a/resources/langs/nheko_pt_PT.ts +++ b/resources/langs/nheko_pt_PT.ts @@ -4,25 +4,25 @@ ActiveCallBar - + Calling... - + A chamar... Connecting... - + A ligar... You are screen sharing - + Estรก a partilhar o seu ecrรฃ Hide/Show Picture-in-Picture - + Mostrar/Ocultar Picture-in-Picture @@ -32,7 +32,7 @@ Mute Mic - + Silenciar microfone @@ -40,262 +40,264 @@ Awaiting Confirmation - + A aguardar confirmaรงรฃo Waiting for other side to complete verification. - + A aguardar que o outro lado complete a verificaรงรฃo. Cancel - + Cancelar CallInvite - + Video Call - + Videochamada Voice Call - + Chamada No microphone found. - + Nenhum microfone encontrado. CallInviteBar - + Video Call - + Videochamada Voice Call - + Chamada Devices - + Dispositivos Accept - + Aceitar Unknown microphone: %1 - + Microfone desconhecido: %1 Unknown camera: %1 - + Cรขmara desconhecida: %1 Decline - + Recusar No microphone found. - + Nenhum microfone encontrado. CallManager - + Entire screen - + Ecrรฃ inteiro ChatPage - + Failed to invite user: %1 - + Falha ao convidar utilizador: %1 - + Invited user: %1 - + Utilizador convidado: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + A migraรงรฃo da cache para a versรฃo atual falhou, e existem vรกrias razรตes possรญveis. Por favor abra um problema ("issue") e experimente usar uma versรฃo mais antiga entretanto. Alternativamente, pode tentar apagar a cache manualmente. - + Confirm join - + Confirmar entrada Do you really want to join %1? - + Tem a certeza que quer entrar em %1? - + Room %1 created. - + Sala %1 criada. - + Confirm invite - + Confirmar convite - + Do you really want to invite %1 (%2)? - + Tem a certeza que quer convidar %1 (%2)? - + Failed to invite %1 to %2: %3 - + Falha ao convidar %1 para %2: %3 Confirm kick - + Confirmar expulsรฃo Do you really want to kick %1 (%2)? - + Tem a certeza que quer expulsar %1 (%2)? - + Kicked user: %1 - + Utilizador expulso: %1 Confirm ban - + Confirmar banimento Do you really want to ban %1 (%2)? - + Tem a certeza que quer banir %1 (%2)? - + Failed to ban %1 in %2: %3 - + Falha ao banir %1 em %2: %3 Banned user: %1 - + Utilizador banido: %1 Confirm unban - + Confirmar perdรฃo Do you really want to unban %1 (%2)? - + Tem a certeza que quer perdoar %1 (%2)? - + Failed to unban %1 in %2: %3 - + Falha ao perdoar %1 em %2: %3 Unbanned user: %1 - + Utilizador perdoado: %1 - + Do you really want to start a private chat with %1? - + Tem a certeza que quer comeรงar uma conversa privada com %1? - + Cache migration failed! - + Falha ao migrar a cache! Incompatible cache version - + Versรฃo da cache incompatรญvel The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache. - + A cache que existe no seu disco รฉ mais recente do que esta versรฃo do Nheko suporta. Por favor atualize-a ou apague-a. - + Failed to restore OLM account. Please login again. - + Falha ao restaurar a sua conta OLM. Por favor autentique-se novamente. + + Failed to restore save data. Please login again. - + Falha ao restaurar dados guardados. Por favor, autentique-se novamente. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - + Falha ao estabelecer chaves encriptadas. Resposta do servidor: %1 %2. Tente novamente mais tarde. - - + + Please try to login again: %1 - + Por favor, tente autenticar-se novamente: %1 - + Failed to join room: %1 - + Falha ao entrar em sala: %1 - + You joined the room - + Entrou na sala Failed to remove invite: %1 - + Falha ao remover convite: %1 - + Room creation failed: %1 - + Falha ao criar sala: %1 Failed to leave room: %1 - + Falha ao sair da sala: %1 - + Failed to kick %1 from %2: %3 - + Falha ao expulsar %1 de %2: %3 @@ -303,7 +305,7 @@ Hide rooms with this tag or from this space by default. - + Ocultar, por defeito, salas com esta etiqueta ou pertencentes a este espaรงo. @@ -311,70 +313,70 @@ All rooms - + Todas as salas Shows all rooms without filtering. - + Mostra todas as salas sem filtros. Favourites - + Favoritos Rooms you have favourited. - + Salas favoritas. Low Priority - + Prioridade baixa Rooms with low priority. - + Salas com prioridade baixa. Server Notices - + Avisos do servidor Messages from your server or administrator. - + Mensagens do seu servidor ou administrador. CrossSigningSecrets - + Decrypt secrets - + Desencriptar segredos Enter your recovery key or passphrase to decrypt your secrets: - + Insira a sua chave de recuperaรงรฃo ou palavra-passe para desencriptar os seus segredos: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Insira a sua chave de recuperaรงรฃo ou palavra-passe chamada %1 para desencriptar os seus segredos: - + Decryption failed - + Falha ao desencriptar Failed to decrypt secrets with the provided recovery key or passphrase - + Falha ao desencriptada segredos com a chave ou palavra-passe dada @@ -382,22 +384,22 @@ Verification Code - + Cรณdigo de verificaรงรฃo Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification! - + Por favor verifique os seguintes dรญgitos. Deve ver os mesmos em ambos os lados. Se forem diferentes, carregue em "Nรฃo coincidem!" para abortar a verificaรงรฃo! They do not match! - + Nรฃo coincidem! They match! - + Coincidem! @@ -405,22 +407,22 @@ Apply - + Aplicar Cancel - + Cancelar Name - + Nome Topic - + Tรณpico @@ -428,47 +430,47 @@ Search - + Procurar - + People - + Pessoas Nature - + Natureza Food - + Comida Activity - + Actividades Travel - + Viagem Objects - + Objetos Symbols - + Sรญmbolos Flags - + Bandeiras @@ -476,22 +478,65 @@ Verification Code - + Cรณdigo de verificaรงรฃo Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification! - + Por favor verifique os seguintes emojis. Deve ver os mesmos em ambos os lados. Se nรฃo coincidirem, carregue em "Nรฃo coincidem!" para abortar a verificaรงรฃo! They do not match! - + Nรฃo coincidem! They match! - + Coincidem! + + + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + Nรฃo existe nenhuma chave para desbloquear esta mensagem. Nรณs pedimos a chave automaticamente, mas pode tentar pedi-la outra vez se estiver impaciente. + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + Esta mensagem nรฃo pรดde ser desencriptada, porque apenas temos uma chave para mensagens mais recentes. Pode tentar solicitar acesso a esta mensagem. + + + + There was an internal error reading the decryption key from the database. + Ocorreu um erro interno ao ler a chave de desencriptaรงรฃo da base de dados. + + + + There was an error decrypting this message. + Ocorreu um erro ao desencriptar esta mensagem. + + + + The message couldn't be parsed. + Esta mensagem nรฃo pรดde ser processada. + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + Esta chave de encriptaรงรฃo foi reutilizada! ร‰ possรญvel que alguรฉm esteja a tentar inserir mensagens falsas nesta conversa! + + + + Unknown decryption error + Erro de desencriptaรงรฃo desconhecido + + + + Request key + Solicitar chave @@ -499,67 +544,22 @@ This message is not encrypted! - + Esta mensagem nรฃo estรก encriptada! Encrypted by a verified device - + Encriptado por um dispositivo verificado. Encrypted by an unverified device, but you have trusted that user so far. - + Encriptado por um dispositivo nรฃo verificado, mas atรฉ agora tem confiado neste utilizador. - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- - + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + Encriptado por um dispositivo nรฃo verificado ou a chave รฉ de uma fonte nรฃo confiรกvel, como o backup da chave. @@ -567,33 +567,42 @@ Verification failed - + Falha ao verifcar Other client does not support our verification protocol. - + O outro cliente nรฃo suporta o nosso protocolo de verificaรงรฃo. Key mismatch detected! + Detetada divergรชncia de chaves! + + + + Device verification timed out. + A verificaรงรฃo do dispositivo expirou. + + + + Other party canceled the verification. + A outra parte cancelou a verificaรงรฃo. + + + + Verification messages received out of order! - - Device verification timed out. + Unknown verification error. - - Other party canceled the verification. - - - - + Close - + Fechar @@ -601,96 +610,212 @@ Forward Message + Reencaminhar mensagem + + + + ImagePackEditorDialog + + + Editing image pack + A editar pacote de imagens + + + + Add images + Adicionar imagens + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + Autocolantes (*.png *.webp *.gif *.jpg *.jpeg) + + + + State key + + + Packname + Nome do pacote + + + + Attribution + + + + + + Use as Emoji + Usar como emoji + + + + + Use as Sticker + Usar como autocolante + + + + Shortcode + Cรณdigo + + + + Body + Corpo + + + + Remove from pack + Remover do pacote + + + + Remove + Remover + + + + Cancel + Cancelar + + + + Save + Guardar + ImagePackSettingsDialog Image pack settings - + Definiรงรตes do pacote de imagens - + + Create account pack + Criar pacote de conta + + + + New room pack + Criar pacote de sala + + + Private pack - + Pacote privado Pack from this room - + Pacote desta sala Globally enabled pack - + Pacote ativo globalmente - + Enable globally - + Ativar globalmente Enables this pack to be used in all rooms - + Permite que o pacote seja usado em todas as salas - + + Edit + Editar + + + Close - + Fechar InputBar - + Select a file - + Selecionar um ficheiro All Files (*) - + Todos os ficheiros (*) - + Failed to upload media. Please try again. - + Falha ao carregar mรญdia. Por favor, tente novamente. InviteDialog - + Invite users to %1 - + Convidar utilizadores para %1 User ID to invite - + ID do utilizador a convidar @joe:matrix.org Example user id. The name 'joe' can be localized however you want. - + @ze:matrix.org Add - + Adicionar Invite - + Convidar Cancel + Cancelar + + + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + + + + + LeaveRoomDialog + + + Leave room + Sair da sala + + + + Are you sure you want to leave? @@ -699,12 +824,12 @@ Matrix ID - + ID Matrix e.g @joe:matrix.org - + p. ex. @ze:matrix.org @@ -712,170 +837,197 @@ You can also put your homeserver address there, if your server doesn't support .well-known lookup. Example: @user:server.my If Nheko fails to discover your homeserver, it will show you a field to enter the server manually. - + O seu nome de utilizador. +Um ID Matrix (MXID) deve iniciar com um @ seguido pelo ID do utilizador, uns :, e por fim, o nome do seu servidor. Pode, tambรฉm, colocar o seu endereรงo, caso este nรฃo suporte pesquisas ".well-known". +Exemplo: @utilizador:servidor.meu +Se o Nheko nรฃo conseguir encontrar o seu servidor, irรก apresentar um campo onde deve inserir o endereรงo manualmente. Password - + Palavra-passe Your password. - + A sua palavra-passe Device name - + Nome do dispositivo A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used. - + Um nome para este dispositivo, que serรก exibido noutros quando os estiver a verificar. Caso nenhum seja fornecido, serรก usado um prรฉ-definido. Homeserver address - + Endereรงo do servidor server.my:8787 - + servidor.meu:8787 The address that can be used to contact you homeservers client API. Example: https://server.my:8787 - + O endereรงo que pode ser usado para contactar a API de clientes do seu servidor. +Exemplo: https://servidor.meu:8787 LOGIN - + INCIAR SESSรƒO - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Inseriu um ID Matrix invรกlido p. ex. @ze:matrix.org - + Autodiscovery failed. Received malformed response. - + Falha na descoberta automรกtica. Reposta mal formatada recebida. - + Autodiscovery failed. Unknown error when requesting .well-known. - + Falha na descoberta automรกtica. Erro desconhecido ao solicitar ".well-known". - + The required endpoints were not found. Possibly not a Matrix server. - + Nรฃo foi possรญvel encontrar os funรงรตes ("endpoints") necessรกrias. Possivelmente nรฃo รฉ um servidor Matrix. Received malformed response. Make sure the homeserver domain is valid. - + Resposta mal formada recebida. Certifique-se que o domรญnio do servidor estรก correto. - + An unknown error occured. Make sure the homeserver domain is valid. - + Erro desconhecido. Certifique-se que o domรญnio do servidor รฉ vรกlido. - + SSO LOGIN - + ENTRAR COM ISU (SSO) - + Empty password + Palavra-passe vazia + + + + SSO login failed + Falha no ISU (SSO) + + + + LogoutDialog + + + Log out - - SSO login failed + + A call is in progress. Log out? + + + + + Are you sure you want to log out? MessageDelegate - + Encryption enabled - + Encriptaรงรฃo ativada - + room name changed to: %1 - + nome da sala alterado para: %1 removed room name - + nome da sala removido topic changed to: %1 - + tรณpico da sala alterado para: %1 removed topic - + tรณpico da sala removido %1 changed the room avatar - + %1 alterou o รญcone da sala %1 created and configured room: %2 - + %1 criou e configurou a sala: %2 %1 placed a voice call. - + %1 iniciou uma chamada de voz. %1 placed a video call. - + %1 iniciou uma chamada de vรญdeo. %1 placed a call. - + %1 iniciou uma chamada. Negotiating call... - + A negociar chamadaโ€ฆ - + + Allow them in + Permitir a entrada + + + %1 answered the call. - + %1 atendeu a chamada. - + removed - + removida - + %1 ended the call. - + %1 terminou a chamada. @@ -883,135 +1035,142 @@ Example: https://server.my:8787 Hang up - + Desligar Place a call - + Iniciar chamada Send a file - + Enviar um ficheiro Write a message... - + Escreva uma mensagemโ€ฆ - + Stickers - + Autocolantes Emoji - + Emoji Send - + Enviar You don't have permission to send messages in this room - + Nรฃo tem permissรฃo para enviar mensagens nesta sala MessageView - + Edit - + Editar React - + Reagir Reply - + Responder Options - + Opรงรตes - + + &Copy - + &Copiar - + + Copy &link location - + Copiar localizaรงรฃo da &ligaรงรฃo - + Re&act - + Re&agir Repl&y - + Responde&r &Edit - + &Editar Read receip&ts - + Recibos de &leitura &Forward - + Reen&caminhar &Mark as read - + &Marcar como lida View raw message - + Ver mensagem bruta View decrypted raw message - + Ver mensagem bruta desencriptada Remo&ve message - + Remo&ver mensagem &Save as - + &Guardar como &Open in external program - + Abrir num &programa externo Copy link to eve&nt - + Copiar ligaรงรฃo para o eve&nto + + + + &Go to quoted message + Ir para mensagem &citada @@ -1019,56 +1178,69 @@ Example: https://server.my:8787 Send Verification Request - + Enviar pedido de verificaรงรฃo Received Verification Request + Pedido de verificaรงรฃo recebido + + + + 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.) - + 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? - + Para que outros possam ver que dispositivos pertencem realmente a si, pode verificรก-los. Isso permite, tambรฉm, que a cรณpia de seguranรงa de chaves funcione automaticamente. Verificar %1 agora? To ensure that no malicious user can eavesdrop on your encrypted communications you can verify the other party. - + Para garantir que nenhum utilizador mal-intencionado possa intercetar as suas comunicaรงรตes encriptadas, pode verificar a outra parte. %1 has requested to verify their device %2. - + %1 requisitou a verificaรงรฃo do seu dispositivo %2. %1 using the device %2 has requested to be verified. - + %1, usando o dispositivo %2, requisitou a sua verificaรงรฃo. Your device (%1) has requested to be verified. - + O seu dispositivo (%1) requisitou a sua verificaรงรฃo. Cancel - + Cancelar Deny - + Recusar Start verification - + Iniciar verificaรงรฃo Accept + Aceitar + + + + NotificationWarning + + + You are about to notify the whole room @@ -1076,44 +1248,32 @@ Example: https://server.my:8787 NotificationsManager - - + + %1 sent an encrypted message - + %1 enviou uma mensagem encriptada - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - + %1 respondeu: %2 %1 replied with an encrypted message - + %1 respondeu com uma mensagem encriptada %1 replied to a message - + %1 respondeu a uma mensagem %1 sent a message - + %1 enviou uma mensagem @@ -1121,32 +1281,32 @@ Example: https://server.my:8787 Place a call to %1? - + Iniciar chamada para %1? No microphone found. - + Nenhum microfone encontrado. - + Voice - + Voz Video - + Vรญdeo Screen - + Ecrรฃ Cancel - + Cancelar @@ -1154,412 +1314,512 @@ Example: https://server.my:8787 unimplemented event: - + evento nรฃo implementado: QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. - + Crie um perfil รบnico que lhe permite entrar em vรกrias contas simultaneamente e iniciar vรกrias instรขncias do Nheko. profile - + perfil profile name - + nome de perfil + + + + ReadReceipts + + + Read receipts + Recibos de leitura + + + + ReadReceiptsModel + + + Yesterday, %1 + Ontem, %1 RegisterPage - + Username - + Nome de utilizador - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + O nome de utilizador nรฃo pode ser vazio e tem que conter apenas os caracteres a-z, 0-9, ., _, =, - e /. - + Password - + Palavra-passe Please choose a secure password. The exact requirements for password strength may depend on your server. - + Por favor, escolha uma palavra-passe segura. Os requisitos exatos para a forรงa da palavra-passe poderรฃo depender no seu servidor. Password confirmation - + Confirmaรงรฃo da palavra-passe Homeserver - + Servidor - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. - + Um servidor que permita registos. Uma vez que a Matrix รฉ descentralizada, o utilizador precisa primeiro de encontrar um servidor onde se possa registar, ou alojar o seu prรณprio. REGISTER - + REGISTAR - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Falha na descoberta automรกtica. Resposta mal formada recebida. - + Autodiscovery failed. Unknown error when requesting .well-known. - + Falha na descoberta automรกtica. Erro desconhecido ao requisitar ".well-known". - + The required endpoints were not found. Possibly not a Matrix server. - + Nรฃo foi possรญvel encontrar os funรงรตes ("endpoints") necessรกrias. Possivelmente nรฃo รฉ um servidor Matrix. Received malformed response. Make sure the homeserver domain is valid. - + Resposta mal formada recebida. Certifique-se que o domรญnio do servidor estรก correto. An unknown error occured. Make sure the homeserver domain is valid. - + Erro desconhecido. Certifique-se que o domรญnio do servidor รฉ vรกlido. - + Password is not long enough (min 8 chars) - + A palavra-passe nรฃo รฉ longa o suficiente (mรญn, 8 caracteres) - + Passwords don't match - + As palavras-passe nรฃo coincidem - + Invalid server name - + Nome do servidor invรกlido ReplyPopup - + Close - + Fechar Cancel edit + Cancelar ediรงรฃo + + + + RoomDirectory + + + Explore Public Rooms + Explorar salas pรบblicas + + + + Search for public rooms + Procurar por salas pรบblicas + + + + Choose custom homeserver RoomInfo - + no version stored - + nenhuma versรฃo guardada RoomList - + New tag - + Nova etiqueta Enter the tag you want to use: - - - - - Leave Room - - - - - Are you sure you want to leave this room? - + Insira a etiqueta que quer usar: Leave room - + Sair da sala Tag room as: - + Etiquetar sala com: Favourite - + Favoritos Low priority - + Prioridade baixa Server notice - + Avisos do servidor Create new tag... - + Criar nova etiqueta... - + Status Message - + Mensagem de estado Enter your status message: - + Insira a sua mensagem de estado: Profile settings - + Definiรงรตes de perfil Set status message - + Definir mensagem de estado - + Logout + Terminar sessรฃo + + + + Encryption not set up + Cross-signing setup has not run yet. - - Start a new chat + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Fechar + + + + Start a new chat + Iniciar uma nova conversa + Join a room - + Entrar numa sala Create a new room - + Criar uma nova sala Room directory - + Diretรณrio de salas - + User settings - + Definiรงรตes de utilizador RoomMembers - + Members of %1 - + Membros de %1 - + %n people in %1 Summary above list of members - - - + + %n pessoa em %1 + %n pessoas em %1 Invite more people - + Convidar mais pessoas + + + + This room is not encrypted! + Esta sala nรฃo estรก encriptada! + + + + This user is verified. + Este utilizador estรก verificado. + + + + This user isn't verified, but is still using the same master key from the first time you met. + Este utilizador nรฃo estรก verificado, mas continua a usar a mesma chave-mestra da primeira vez que se conheceram. + + + + This user has unverified devices! + Este utilizador tem dispositivos nรฃo verificados! RoomSettings - + Room Settings - + Definiรงรตes de sala - + %1 member(s) - + %1 membro(s) SETTINGS - + DEFINIร‡ลŽES Notifications - + Notificaรงรตes Muted - + Silenciada Mentions only - + Apenas menรงรตes All messages - + Todas as mensagens - + + Room access + Acesso ร  sala + + + Anyone and guests - + Qualquer pessoa e visitantes Anyone - + Qualquer pessoa Invited users - + Utilizadores convidados - + + By knocking + "Batendo ร  porta" + + + + Restricted by membership in other rooms + Impedido por participaรงรฃo noutras salas + + + Encryption - + Encriptaรงรฃo End-to-End Encryption - + Encriptaรงรฃo ponta-a-ponta Encryption is currently experimental and things might break unexpectedly. <br> Please take note that it can't be disabled afterwards. - + A encriptaรงรฃo รฉ, de momento, experimental e certas coisas podem partir-se inesperadamente.<br>Por favor, tome nota de que depois nรฃo pode ser desativada. Sticker & Emote Settings - + Definiรงรตes de autocolantes e emojis Change - + Alterar Change what packs are enabled, remove packs or create new ones - + Alterar a seleรงรฃo de pacotes ativos, remover e criar novos pacotes INFO - + INFO Internal ID - + ID interno Room Version - + Versรฃo da sala - + Failed to enable encryption: %1 - + Falha ao ativar encriptaรงรฃo: %1 - + Select an avatar - + Selecionar um รญcone All Files (*) - + Todos os ficheiros (*) The selected file is not an image - + O ficheiro selecionado nรฃo รฉ uma imagem Error while reading file: %1 - + Erro ao ler ficheiro: %1 - - + + Failed to upload image: %s - + Falha ao carregar imagem: %s RoomlistModel - + Pending invite. - + Convite pendente. - + Previewing this room + A prรฉ-visualizar esta sala + + + + No preview available + Prรฉ-visualizaรงรฃo nรฃo disponรญvel + + + + Root + + + Please enter your login password to continue: - - No preview available + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. @@ -1568,27 +1828,27 @@ Example: https://server.my:8787 Share desktop with %1? - + Partilhar ambiente de trabalho com %1? Window: - + Janela: Frame rate: - + Taxa de fotogramas: Include your camera picture-in-picture - + Incluir a sua cรขmara em miniatura Request remote camera - + Requisitar cรขmara remota @@ -1599,45 +1859,160 @@ Example: https://server.my:8787 Hide mouse cursor - + Esconder cursor do rato Share - + Partilhar Preview - + Prรฉ-visualizar Cancel + Cancelar + + + + SecretStorage + + + Failed to connect to secret storage + Falha ao ligar ao armazenamento secreto + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + Falha ao atualizar pacote de imagem: %1 + + + + Failed to delete old image pack: %1 + Falha ao eliminar pacote de imagem antigo: %1 + + + + Failed to open image: %1 + Falha ao abrir imagem: %1 + + + + Failed to upload image: %1 + Falha ao carregar imagem: %1 + + StatusIndicator Failed - + Falhou Sent - + Enviado Received - + Recebido Read - + Lido @@ -1645,7 +2020,7 @@ Example: https://server.my:8787 Search - + Procurar @@ -1653,283 +2028,315 @@ Example: https://server.my:8787 Successful Verification - + Verificaรงรฃo bem sucedida Verification successful! Both sides verified their devices! - + Verificaรงรฃo bem sucedida! Ambos os lados verificaram os seus dispositivos! Close - + Fechar TimelineModel - + Message redaction failed: %1 - + Falha ao eliminar mensagem: %1 - + Failed to encrypt event, sending aborted! - + Falha ao encriptar evento, envio abortado! - + Save image - + Guardar imagem Save video - + Guardar vรญdeo Save audio - + Guardar รกudio Save file - + Guardar ficheiro - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) - - - + + %1%2 estรก a escrever... + %1 e %2 estรฃo a escrever... - + %1 opened the room to the public. - + %1 abriu a sala ao pรบblico. %1 made this room require and invitation to join. - + %1 tornou esta sala acessรญvel apenas por convite. - + + %1 allowed to join this room by knocking. + %1 tornou possรญvel entrar na sala "batendo ร  porta". + + + + %1 allowed members of the following rooms to automatically join this room: %2 + %1 autorizou os membros das seguintes salas a juntarem-se ร  sala automaticamente: %2 + + + %1 made the room open to guests. - + %1 tornou a sala aberta a visitantes. %1 has closed the room to guest access. - + %1 fechou o acesso ร  sala a visitantes. %1 made the room history world readable. Events may be now read by non-joined people. - + %1 tornou o histรณrico da sala visรญvel a qualquer pessoa. Eventos podem agora ser lidos por nรฃo-membros. %1 set the room history visible to members from this point on. - + %1 tornou o histรณrico da sala, a partir deste momento, visรญvel a membros - + %1 set the room history visible to members since they were invited. - + %1 tornou o histรณrico visรญvel a membros desde o seu convite. - + %1 set the room history visible to members since they joined the room. - + %1 tornou o histรณrico da sala visรญvel ao membros desde a sua entrada. %1 has changed the room's permissions. - + %1 alterou as permissรตes da sala. - + %1 was invited. - + %1 foi convidado. - + %1 changed their avatar. - + %1 alterou o seu avatar. %1 changed some profile info. - + %1 alterou alguma informaรงรฃo de perfil. - + %1 joined. - + %1 entrou. - + + %1 joined via authorisation from %2's server. + %1 entrou com autorizaรงรฃo do servidor de %2. + + + %1 rejected their invite. - + %1 recusou o seu convite. Revoked the invite to %1. - + Convite de %1 cancelado. %1 left the room. - + %1 saiu da sala. Kicked %1. - + %1 foi expulso. Unbanned %1. - + %1 foi perdoado. %1 was banned. - + %1 foi banido. - + Reason: %1 - + Razรฃo: %1 - + %1 redacted their knock. - + %1 eliminou a sua "batida ร  porta". - + You joined this room. - + Entrou na sala. - + %1 has changed their avatar and changed their display name to %2. - + %1 alterou o seu avatar e tambรฉm o seu nome de exibiรงรฃo para %2. - + %1 has changed their display name to %2. - + %1 alterou o seu nome de exibiรงรฃo para %2. - + Rejected the knock from %1. - + Recusada a batida de %1. %1 left after having already left! This is a leave event after the user already left and shouldn't happen apart from state resets - + %1 saiu depois de jรก ter saรญdo! %1 knocked. - + %1 bateu ร  porta. TimelineRow - + Edited - + Editada TimelineView - + No room open - + Nenhuma sala aberta - + + No preview available + Prรฉ-visualizaรงรฃo nรฃo disponรญvel + + + %1 member(s) - + %1 membro(s) join the conversation - + juntar-se ร  conversa accept invite - + aceitar convite decline invite - + recusar convite Back to room list - - - - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - + Voltar ร  lista de salas TopBar - + Back to room list - + Voltar ร  lista de salas - + No room selected + Nenhuma sala selecionada + + + + This room is not encrypted! + Esta sala nรฃo รฉ encriptada! + + + + This room contains only verified devices. + Esta sala contรฉm apenas dispositivos verificados. + + + + This room contains verified devices and devices which have never changed their master key. - + + This room contains unverified devices! + Esta sala contรฉm dispositivos nรฃo verificados! + + + Room options - + Opรงรตes da sala Invite users - + Convidar utilizadores Members - + Membros Leave room - + Sair da sala Settings - + Definiรงรตes @@ -1937,123 +2344,213 @@ Example: https://server.my:8787 Show - + Mostrar Quit + Sair + + + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + Por favor, insira um testemunho de registo vรกlido. + + + + Invalid token UserProfile - + Global User Profile - + Perfil de utilizador global Room User Profile - + Perfil de utilizador na sala - - + + Change avatar globally. + Alterar avatar globalmente. + + + + Change avatar. Will only apply to this room. + Alterar avatar. Irรก apenas afetar esta sala. + + + + Change display name globally. + Alterar nome de exibiรงรฃo globalmente. + + + + Change display name. Will only apply to this room. + Alterar nome de exibiรงรฃo. Irรก apenas afetar esta sala. + + + + Room: %1 + Sala: %1 + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + Este รฉ um perfil especรญfico desta sala. O nome e avatar do utilizador poderรฃo ser diferentes dos seus homรณlogos globais. + + + + Open the global profile for this user. + Abrir o perfil global deste utilizador. + + + + Verify - + Verificar - - Ban the user - - - - - Start a private chat - + + Start a private chat. + Iniciar uma conversa privada. - Kick the user + Kick the user. + Expulsar o utilizador. + + + + Ban the user. + Banir o utilizador. + + + + Refresh device list. - + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify + Anular verificaรงรฃo + + + + Sign out device %1 - - Select an avatar + + You signed out this device. + + + Select an avatar + Selecionar um avatar + All Files (*) - + Todos os ficheiros (*) The selected file is not an image - + O ficheiro selecionado nรฃo รฉ uma imagem Error while reading file: %1 - + Erro ao ler ficheiro: %1 UserSettings - - + + Default - + Padrรฃo UserSettingsPage - + Minimize to tray - + Minimizar para bandeja Start in tray - + Iniciar na bandeja - + Group's sidebar - + Barra lateral do grupo - + Circular Avatars - + Avatares circulares - + profile: %1 - + perfil: %1 - + Default - + Padrรฃo CALLS - + CHAMADAS Cross Signing Keys - + Chaves de assinatura cruzada @@ -2063,12 +2560,12 @@ Example: https://server.my:8787 DOWNLOAD - + DESCARREGAR - + Keep the application running in the background after closing the client window. - + Manter a aplicaรงรฃo a correr em segundo plano depois de fechar a janela. @@ -2081,6 +2578,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2100,25 +2607,28 @@ Only affects messages in encrypted chats. Privacy Screen - + Ecrรฃ de privacidade When the window loses focus, the timeline will be blurred. - + Quando a janela perde a atenรงรฃo, a cronologia +serรก desfocada. - + Privacy screen timeout (in seconds [0 - 3600]) - + Tempo de inatividade para ecrรฃ de privacidade (em segundos [0 - 3600]) Set timeout (in seconds) for how long after window loses focus before the screen will be blurred. Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) - + Definir tempo (em segundos) depois da janela perder a +atenรงรฃo atรฉ que o ecrรฃ seja desfocado. +Defina como 0 para desfocar imediatamente apรณs perder a atenรงรฃo. Valor mรกximo de 1 hora (3600 segundos) @@ -2143,41 +2653,45 @@ Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds Typing notifications - + Notificaรงรตes de escrita Show who is typing in a room. This will also enable or disable sending typing notifications to others. - + Mostrar quem estรก a escrever numa sala. +Irรก tambรฉm ativar ou desativar o envio de notificaรงรตes de escrita para outros. Sort rooms by unreads - + Ordenar salas por nรฃo lidas Display rooms with new messages first. If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room. If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. - + Exibe salas com novas mensagens primeiro. +Se desativada, a lista de salas serรก apenas ordenada pela data da รบltima mensagem de cada sala. +Se ativada, salas com notificaรงรตes ativas (pequeno cรญrculo com um nรบmero dentro) serรฃo ordenadas no topo. Salas silenciadas continuarรฃo a ser ordenadas por data, visto que nรฃo sรฃo consideradas tรฃo importantes como as outras. - + Read receipts - + Recibos de leitura Show if your message was read. Status is displayed next to timestamps. - + Mostrar se a sua mensagem foi lida. +Estado exibido ao lado da data. - + Send messages as Markdown - + Enviar mensagens como Markdown @@ -2187,6 +2701,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications @@ -2227,12 +2746,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2242,7 +2796,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2317,7 +2871,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -2337,17 +2891,22 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2362,12 +2921,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2387,7 +2941,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2409,22 +2963,22 @@ This usually causes the application icon in the task bar to animate in some fash Select a file - + Selecionar um ficheiro All Files (*) - + Todos os ficheiros (*) - + Open Sessions File - + @@ -2432,19 +2986,19 @@ This usually causes the application icon in the task bar to animate in some fash - - + + File Password - + Enter the passphrase to decrypt the file: - + The password cannot be empty @@ -2459,6 +3013,14 @@ This usually causes the application icon in the task bar to animate in some fash + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Nรฃo foi encontrada nenhuma conversa privada e encriptada com este utilizador. Crie uma e tente novamente. + + Waiting @@ -2484,7 +3046,7 @@ This usually causes the application icon in the task bar to animate in some fash Cancel - + Cancelar @@ -2502,12 +3064,12 @@ This usually causes the application icon in the task bar to animate in some fash REGISTER - + REGISTAR LOGIN - + INCIAR SESSรƒO @@ -2528,17 +3090,17 @@ This usually causes the application icon in the task bar to animate in some fash Cancel - + Cancelar Name - + Nome Topic - + Tรณpico @@ -2571,7 +3133,7 @@ This usually causes the application icon in the task bar to animate in some fash Cancel - + Cancelar @@ -2584,43 +3146,12 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - - - - - Room ID or alias - - - - - dialogs::LeaveRoom - - - Cancel - - - - - Are you sure you want to leave? - - - dialogs::Logout Cancel - + Cancelar @@ -2638,7 +3169,7 @@ This usually causes the application icon in the task bar to animate in some fash Cancel - + Cancelar @@ -2653,7 +3184,7 @@ Media size: %2 Cancel - + Cancelar @@ -2666,32 +3197,6 @@ Media size: %2 - - dialogs::ReadReceipts - - - Read receipts - - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2705,47 +3210,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2760,9 +3265,9 @@ Media size: %2 - + %1: %2 - + %1: %2 @@ -2772,7 +3277,7 @@ Media size: %2 %1 sent an encrypted message - + %1 enviou uma mensagem encriptada @@ -2780,27 +3285,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2808,7 +3313,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/langs/nheko_ro.ts b/resources/langs/nheko_ro.ts index 6ea496f1..6d28da46 100644 --- a/resources/langs/nheko_ro.ts +++ b/resources/langs/nheko_ro.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... @@ -56,7 +56,7 @@ CallInvite - + Video Call @@ -74,7 +74,7 @@ CallInviteBar - + Video Call @@ -117,7 +117,7 @@ CallManager - + Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 Nu s-a putut invita utilizatorul: %1 - + Invited user: %1 Utilizator invitat: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. Nu s-a putut muta cache-ul pe versiunea curentฤƒ. Acest lucru poate avea diferite cauze. Vฤƒ rugฤƒm sฤƒ deschideศ›i un issue ศ™i รฎncercaศ›i sฤƒ folosiศ›i o versiune mai veche รฎntre timp. O altฤƒ opศ›iune ar fi sฤƒ รฎncercaศ›i sฤƒ ศ™tergeศ›i cache-ul manual. - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. Camera %1 a fost creatฤƒ. - + Confirm invite - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 Nu s-a putut invita %1 รฎn %2: %3 @@ -182,7 +182,7 @@ - + Kicked user: %1 Utilizator eliminat: %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 Nu s-a putut interzice utilizatorul %1 รฎn %2: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 Nu s-a putut dezinterzice %1 รฎn %2: %3 @@ -227,12 +227,12 @@ Utilizator dezinterzis: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! Nu s-a putut migra cache-ul! @@ -247,33 +247,35 @@ Cache-ul de pe disc este mai nou decรขt versiunea pe care Nheko o suportฤƒ. Vฤƒ rugฤƒm actualizaศ›i sau ศ™tergeศ›i cache-ul. - + Failed to restore OLM account. Please login again. Nu s-a putut restabili contul OLM. Vฤƒ rugฤƒm sฤƒ vฤƒ reconectaศ›i. + + Failed to restore save data. Please login again. Nu s-au putut restabili datele salvate. Vฤƒ rugฤƒm sฤƒ vฤƒ reconectaศ›i. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Nu s-au putut stabili cheile. Rฤƒspunsul serverului: %1 %2. Vฤƒ rugฤƒm รฎncercaศ›i mai tรขrziu. - - + + Please try to login again: %1 Vฤƒ rugฤƒm sฤƒ vฤƒ reconectaศ›i: %1 - + Failed to join room: %1 Nu s-a putut alฤƒtura la camerฤƒ: %1 - + You joined the room V-aศ›i alฤƒturat camerei @@ -283,7 +285,7 @@ Nu s-a putut ศ™terge invitaศ›ia: %1 - + Room creation failed: %1 Nu s-a putut crea camera: %1 @@ -293,7 +295,7 @@ Nu s-a putut pฤƒrฤƒsi camera: %1 - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close รŽnchide @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close รŽnchide @@ -645,7 +744,7 @@ InputBar - + Select a file @@ -655,7 +754,7 @@ Toate fiศ™ierele (*) - + Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + IDul camerei sau alias + + + + LeaveRoomDialog + + + Leave room + Pฤƒrฤƒseศ™te camera + + + + Are you sure you want to leave? + Sigur vrei sฤƒ pฤƒrฤƒseศ™ti camera? + + LoginPage @@ -760,25 +885,25 @@ Exemplu: https://serverul.meu:8787 CONECTARE - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. Autodescoperirea a eศ™uat. Rฤƒspunsul primit este defectuos. - + Autodiscovery failed. Unknown error when requesting .well-known. Autodescoperirea a eศ™uat. Eroare necunoscutฤƒ la solicitarea .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Punctele finale necesare nu au fost gฤƒsite. Posibil a nu fi un server Matrix. @@ -788,30 +913,48 @@ Exemplu: https://serverul.meu:8787 Rฤƒspuns eronat primit. Verificaศ›i ca domeniul homeserverului sฤƒ fie valid. - + An unknown error occured. Make sure the homeserver domain is valid. A apฤƒrut o eroare necunoscutฤƒ. Verificaศ›i ca domeniul homeserverului sฤƒ fie valid. - + SSO LOGIN CONECTARE SSO - + Empty password Parolฤƒ necompletatฤƒ - + SSO login failed Conectarea SSO a eศ™uat + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed @@ -822,7 +965,7 @@ Exemplu: https://serverul.meu:8787 Criptare activatฤƒ - + room name changed to: %1 numele camerei schimbat la: %1 @@ -881,6 +1024,11 @@ Exemplu: https://serverul.meu:8787 Negotiating call... + + + Allow them in + + MessageInput @@ -905,7 +1053,7 @@ Exemplu: https://serverul.meu:8787 - + Stickers @@ -928,7 +1076,7 @@ Exemplu: https://serverul.meu:8787 MessageView - + Edit @@ -948,17 +1096,19 @@ Exemplu: https://serverul.meu:8787 Opศ›iuni - + + &Copy - + + Copy &link location - + Re&act @@ -1017,6 +1167,11 @@ Exemplu: https://serverul.meu:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1031,7 +1186,12 @@ Exemplu: https://serverul.meu:8787 - + + 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.) + + + + 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? @@ -1076,33 +1236,29 @@ Exemplu: https://serverul.meu:8787 Acceptare + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 a trimis un mesaj criptat - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,7 +1289,7 @@ Exemplu: https://serverul.meu:8787 - + Voice @@ -1164,7 +1320,7 @@ Exemplu: https://serverul.meu:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1179,21 +1335,37 @@ Exemplu: https://serverul.meu:8787 + + ReadReceipts + + + Read receipts + Confirmฤƒri de citire + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username Nume de utilizator - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. Numele de utilizator nu poate fi gol, ศ™i trebuie sฤƒ conศ›inฤƒ doar caracterele a-z, 0-9, ., =, - ศ™i /. - + Password Parolฤƒ @@ -1213,7 +1385,7 @@ Exemplu: https://serverul.meu:8787 Homeserver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. Un server care permite รฎnregistrarea. Deoarece Matrix este decentralizat, trebuie sฤƒ gฤƒsiศ›i un server pe care sฤƒ vฤƒ รฎnregistraศ›i sau sฤƒ vฤƒ gฤƒzduiศ›i propriul server. @@ -1223,27 +1395,17 @@ Exemplu: https://serverul.meu:8787 รŽNREGISTRARE - - No supported registration flows! - Fluxuri de รฎnregistrare nesuportate! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. Autodescoperirea a eศ™uat. Rฤƒspunsul primit este defectuos. - + Autodiscovery failed. Unknown error when requesting .well-known. Autodescoperirea a eศ™uat. Eroare necunoscutฤƒ la solicitarea .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Punctele finale necesare nu au fost gฤƒsite. Posibil a nu fi un server Matrix. @@ -1258,17 +1420,17 @@ Exemplu: https://serverul.meu:8787 A apฤƒrut o eroare necunoscutฤƒ. Verificaศ›i ca domeniul homeserverului sฤƒ fie valid. - + Password is not long enough (min 8 chars) Parola nu este destul de lungฤƒ (minim 8 caractere) - + Passwords don't match Parolele nu se potrivesc - + Invalid server name Nume server invalid @@ -1276,7 +1438,7 @@ Exemplu: https://serverul.meu:8787 ReplyPopup - + Close รŽnchide @@ -1286,10 +1448,28 @@ Exemplu: https://serverul.meu:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored nicio versiune stocatฤƒ @@ -1297,7 +1477,7 @@ Exemplu: https://serverul.meu:8787 RoomList - + New tag @@ -1306,16 +1486,6 @@ Exemplu: https://serverul.meu:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1347,7 +1517,7 @@ Exemplu: https://serverul.meu:8787 - + Status Message @@ -1367,12 +1537,35 @@ Exemplu: https://serverul.meu:8787 - + Logout Deconectare - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + รŽnchide + + + Start a new chat รŽncepe o nouฤƒ conversaศ›ie @@ -1392,7 +1585,7 @@ Exemplu: https://serverul.meu:8787 Registru de camere - + User settings Setฤƒri utilizator @@ -1400,12 +1593,12 @@ Exemplu: https://serverul.meu:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1419,16 +1612,36 @@ Exemplu: https://serverul.meu:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1458,7 +1671,12 @@ Exemplu: https://serverul.meu:8787 - + + Room access + + + + Anyone and guests @@ -1473,7 +1691,17 @@ Exemplu: https://serverul.meu:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1519,12 +1747,12 @@ Exemplu: https://serverul.meu:8787 - + Failed to enable encryption: %1 Nu s-a putut activa criptarea: %1 - + Select an avatar Selecteazฤƒ un avatar @@ -1544,8 +1772,8 @@ Exemplu: https://serverul.meu:8787 Eroare รฎntรขmpinatฤƒ la citirea fiศ™ierului: %1 - - + + Failed to upload image: %s Nu s-a putut รฎncฤƒrca imaginea: %s @@ -1553,21 +1781,49 @@ Exemplu: https://serverul.meu:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1622,6 +1878,121 @@ Exemplu: https://serverul.meu:8787 + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1674,18 +2045,18 @@ Exemplu: https://serverul.meu:8787 TimelineModel - + Message redaction failed: %1 Redactare mesaj eศ™uatฤƒ: %1 - + Failed to encrypt event, sending aborted! - + Save image Salvaศ›i imaginea @@ -1705,7 +2076,7 @@ Exemplu: https://serverul.meu:8787 Salvaศ›i fiศ™ier - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1715,7 +2086,7 @@ Exemplu: https://serverul.meu:8787 - + %1 opened the room to the public. %1 a deschis camera publicului. @@ -1725,7 +2096,17 @@ Exemplu: https://serverul.meu:8787 %1 a fฤƒcut ca aceastฤƒ camerฤƒ sฤƒ necesite o invitaศ›ie pentru alฤƒturare. - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. %1 a deschis camera pentru vizitatori. @@ -1745,12 +2126,12 @@ Exemplu: https://serverul.meu:8787 %1 a fฤƒcut istoricul camerei accesibil membrilor din acest moment. - + %1 set the room history visible to members since they were invited. %1 a setat istoricul camerei accesibil participanศ›ilor din momentul invitฤƒrii. - + %1 set the room history visible to members since they joined the room. %1 a setat istoricul camerei accesibil membrilor din momentul alฤƒturฤƒrii. @@ -1760,12 +2141,12 @@ Exemplu: https://serverul.meu:8787 %1 a modificat permisiunile camerei. - + %1 was invited. %1 a fost invitat(ฤƒ). - + %1 changed their avatar. %1 ศ™i-a schimbat avatarul. @@ -1775,12 +2156,17 @@ Exemplu: https://serverul.meu:8787 %1 ศ™i-a schimbat niศ™te informaศ›ii de pe profil. - + %1 joined. %1 s-a alฤƒturat. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. %1 a respins invitaศ›ia. @@ -1810,32 +2196,32 @@ Exemplu: https://serverul.meu:8787 %1 a fost interzis(ฤƒ). - + Reason: %1 - + %1 redacted their knock. %1 ศ™i-a redactat ciocฤƒnitul. - + You joined this room. Te-ai alฤƒturat camerei. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. Ciocฤƒnit refuzat de la %1. @@ -1854,7 +2240,7 @@ Exemplu: https://serverul.meu:8787 TimelineRow - + Edited @@ -1862,12 +2248,17 @@ Exemplu: https://serverul.meu:8787 TimelineView - + No room open Nicio camerฤƒ deschisฤƒ - + + No preview available + + + + %1 member(s) @@ -1892,28 +2283,40 @@ Exemplu: https://serverul.meu:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options @@ -1951,10 +2354,35 @@ Exemplu: https://serverul.meu:8787 Ieศ™ire + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1964,33 +2392,98 @@ Exemplu: https://serverul.meu:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar Selecteazฤƒ un avatar @@ -2013,8 +2506,8 @@ Exemplu: https://serverul.meu:8787 UserSettings - - + + Default @@ -2022,7 +2515,7 @@ Exemplu: https://serverul.meu:8787 UserSettingsPage - + Minimize to tray Minimizeazฤƒ รฎn bara de notificฤƒri @@ -2032,22 +2525,22 @@ Exemplu: https://serverul.meu:8787 Porneศ™te รฎn bara de notificฤƒri - + Group's sidebar Bara lateralฤƒ a grupului - + Circular Avatars Avatare rotunde - + profile: %1 - + Default @@ -2072,7 +2565,7 @@ Exemplu: https://serverul.meu:8787 - + Keep the application running in the background after closing the client window. @@ -2087,6 +2580,16 @@ Exemplu: https://serverul.meu:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2115,7 +2618,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2170,7 +2673,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts Confirmฤƒri de citire @@ -2181,7 +2684,7 @@ Status is displayed next to timestamps. Vezi dacฤƒ mesajul tฤƒu a fost citit. Starea este afiศ™atฤƒ lรขngฤƒ timestampuri. - + Send messages as Markdown Trimite mesaje ca Markdown @@ -2193,6 +2696,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications Notificฤƒri desktop @@ -2233,12 +2741,47 @@ This usually causes the application icon in the task bar to animate in some fash Mฤƒreศ™te fontul mesajelor dacฤƒ doar cรขteva emojiuri sunt afiศ™ate. - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2248,7 +2791,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor Factor de dimensiune @@ -2323,7 +2866,7 @@ This usually causes the application icon in the task bar to animate in some fash Amprentฤƒ Dispozitiv - + Session Keys Chei de sesiune @@ -2343,17 +2886,22 @@ This usually causes the application icon in the task bar to animate in some fash CRIPTARE - + GENERAL GENERAL - + INTERFACE INTERFAศšฤ‚ - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2368,12 +2916,7 @@ This usually causes the application icon in the task bar to animate in some fash Familia de font pentru Emoji - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2393,7 +2936,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2423,14 +2966,14 @@ This usually causes the application icon in the task bar to animate in some fash Toate fiศ™ierele (*) - + Open Sessions File Deschide fiศ™ierul de sesiuni - + @@ -2438,19 +2981,19 @@ This usually causes the application icon in the task bar to animate in some fash Eroare - - + + File Password Parolฤƒ fiศ™ier - + Enter the passphrase to decrypt the file: Introduceศ›i parola pentru a decripta fiศ™ierul: - + The password cannot be empty Parola nu poate fi necompletatฤƒ @@ -2465,6 +3008,14 @@ This usually causes the application icon in the task bar to animate in some fash Fiศ™ier pentru salvarea cheilor de sesiune exportate + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2590,37 +3141,6 @@ This usually causes the application icon in the task bar to animate in some fash Deschideศ›i fallback, urmฤƒriศ›i paศ™ii ศ™i confirmaศ›i dupฤƒ ce i-aศ›i completat. - - dialogs::JoinRoom - - - Join - Alฤƒturare - - - - Cancel - Anulare - - - - Room ID or alias - IDul camerei sau alias - - - - dialogs::LeaveRoom - - - Cancel - Anulare - - - - Are you sure you want to leave? - Sigur vrei sฤƒ pฤƒrฤƒseศ™ti camera? - - dialogs::Logout @@ -2674,32 +3194,6 @@ Dimensiune media: %2 Rezolvฤƒ reCAPTCHA ศ™i apasฤƒ butonul de confirmare - - dialogs::ReadReceipts - - - Read receipts - Confirmฤƒri de citire - - - - Close - รŽnchide - - - - dialogs::ReceiptItem - - - Today %1 - Azi %1 - - - - Yesterday %1 - Ieri %1 - - message-description sent: @@ -2713,47 +3207,47 @@ Dimensiune media: %2 %1 a trimis un clip audio - + You sent an image Ai trimis o imagine - + %1 sent an image %1 a trimis o imagine - + You sent a file Ai trimis un fiศ™ier - + %1 sent a file %1 a trimis un fiศ™ier - + You sent a video Ai trimis un video - + %1 sent a video %1 a trimis un video - + You sent a sticker Ai trimis un sticker - + %1 sent a sticker %1 a trimis un sticker - + You sent a notification Ai trimis o notificare @@ -2768,7 +3262,7 @@ Dimensiune media: %2 Tu: %1 - + %1: %2 %1: %2 @@ -2788,27 +3282,27 @@ Dimensiune media: %2 Ai iniศ›iat un apel - + %1 placed a call %1 a iniศ›iat un apel - + You answered a call Ai rฤƒspuns unui apel - + %1 answered a call %1 a rฤƒspuns unui apel - + You ended a call Ai รฎncheiat un apel - + %1 ended a call %1 a รฎncheiat un apel @@ -2816,7 +3310,7 @@ Dimensiune media: %2 utils - + Unknown Message Type Tip mesaj necunoscut diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 67a306f2..698d277f 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... ะ’ั‹ะทะพะฒ... @@ -56,7 +56,7 @@ CallInvite - + Video Call ะ’ะธะดะตะพ ะ—ะฒะพะฝะพะบ @@ -74,7 +74,7 @@ CallInviteBar - + Video Call ะ’ะธะดะตะพ ะ—ะฒะพะฝะพะบ @@ -117,7 +117,7 @@ CallManager - + Entire screen ะ’ะตััŒ ัะบั€ะฐะฝ @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 ะะต ัƒะดะฐะปะพััŒ ะฟั€ะธะณะปะฐัะธั‚ัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั: %1 - + Invited user: %1 ะŸั€ะธะณะปะฐัˆะตะฝะฝั‹ะน ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒ: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. ะœะธะณั€ะฐั†ะธั ะบััˆะฐ ะดะปั ั‚ะตะบัƒั‰ะตะน ะฒะตั€ัะธะธ ะฝะต ัƒะดะฐะปะฐััŒ. ะญั‚ะพ ะผะพะถะตั‚ ะฟั€ะพะธัั…ะพะดะธั‚ัŒ ะฟะพ ั€ะฐะทะฝั‹ะผ ะฟั€ะธั‡ะธะฝะฐะผ. ะŸะพะถะฐะปัƒะนัั‚ะฐ ัะพะพะฑั‰ะธั‚ะต ะพ ะฟั€ะพะฑะปะตะผะต ะธ ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะฒั€ะตะผะตะฝะฝะพ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ัั‚ะฐั€ัƒัŽ ะฒะตั€ัะธัŽ. ะขะฐะบ-ะถะต ะฒั‹ ะผะพะถะตั‚ะต ะฟะพะฟั€ะพะฑะพะฒะฐั‚ัŒ ัƒะดะฐะปะธั‚ัŒ ะบััˆ ัะฐะผะพัั‚ะพัั‚ะตะปัŒะฝะพ. - + Confirm join ะŸะพะดั‚ะฒะตั€ะดะธั‚ัŒ ะฒั…ะพะด @@ -151,23 +151,23 @@ ะ’ั‹ ะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝะพ ั…ะพั‚ะธั‚ะต ะฟั€ะธัะพะตะดะธะฝะธั‚ัŒัั? - + Room %1 created. ะšะพะผะฝะฐั‚ะฐ %1 ัะพะทะดะฐะฝะฐ. - + Confirm invite ะŸะพะดั‚ะฒะตั€ะดะธั‚ะต ะฟั€ะธะณะปะฐัˆะตะฝะธะต - + Do you really want to invite %1 (%2)? ะ’ั‹ ั‚ะพั‡ะฝะพ ั…ะพั‚ะธั‚ะต ะฟั€ะธะณะปะฐัะธั‚ัŒ %1 (%2)? - + Failed to invite %1 to %2: %3 ะะต ัƒะดะฐะปะพััŒ ะฟั€ะธะณะปะฐัะธั‚ัŒ %1 ะฒ %2: %3 @@ -182,7 +182,7 @@ ะ’ั‹ ั‚ะพั‡ะฝะพ ั…ะพั‚ะธั‚ะต ะฒั‹ะณะฝะฐั‚ัŒ %1 (%2)? - + Kicked user: %1 ะ’ั‹ะณะฝะฐะฝะฝั‹ะน ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒ: %1 @@ -197,7 +197,7 @@ ะ’ั‹ ั‚ะพั‡ะฝะพ ั…ะพั‚ะธั‚ะต ะทะฐะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ %1 (%2)? - + Failed to ban %1 in %2: %3 ะะต ัƒะดะฐะปะพััŒ ะทะฐะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ %1 ะฒ %2: %3 @@ -217,7 +217,7 @@ ะ’ั‹ ั‚ะพั‡ะฝะพ ั…ะพั‚ะธั‚ะต ั€ะฐะทะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ %1 (%2)? - + Failed to unban %1 in %2: %3 ะะต ัƒะดะฐะปะพััŒ ั€ะฐะทะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ %1 ะฒ %2: %3 @@ -227,12 +227,12 @@ ะ ะฐะทะฑะปะพะบะธั€ะพะฒะฐะฝะฝั‹ะน ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒ: %1 - + Do you really want to start a private chat with %1? ะ’ั‹ ะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝะพ ั…ะพั‚ะธั‚ะต ะฝะฐั‡ะฐั‚ัŒ ะปะธั‡ะฝัƒัŽ ะฟะตั€ะตะฟะธัะบัƒ ั %1? - + Cache migration failed! ะœะธะณั€ะฐั†ะธั ะบััˆะฐ ะฝะต ัƒะดะฐะปะฐััŒ! @@ -247,33 +247,35 @@ ะ’ะฐัˆ ะบััˆ ะฝะพะฒะตะต, ั‡ะตะผ ัั‚ะฐ ะฒะตั€ัะธั Nheko ะฟะพะดะดะตั€ะถะธะฒะฐะตั‚. ะŸะพะถะฐะปัƒะนัั‚ะฐ ะพะฑะฝะพะฒะธั‚ะตััŒ ะธะปะธ ะพั‚ั‡ะธัั‚ะธั‚ะต ะฒะฐัˆ ะบััˆ. - + Failed to restore OLM account. Please login again. ะะต ัƒะดะฐะปะพััŒ ะฒะพััั‚ะฐะฝะพะฒะธั‚ัŒ ัƒั‡ะตั‚ะฝัƒัŽ ะทะฐะฟะธััŒ OLM. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฒะพะนะดะธั‚ะต ัะฝะพะฒะฐ. + + Failed to restore save data. Please login again. ะะต ัƒะดะฐะปะพััŒ ะฒะพััั‚ะฐะฝะพะฒะธั‚ัŒ ัะพั…ั€ะฐะฝะตะฝะฝั‹ะต ะดะฐะฝะฝั‹ะต. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฒะพะนะดะธั‚ะต ัะฝะพะฒะฐ. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. ะะต ัƒะดะฐะปะพััŒ ะฝะฐัั‚ั€ะพะธั‚ัŒ ะบะปัŽั‡ะธ ัˆะธั„ั€ะพะฒะฐะฝะธั. ะžั‚ะฒะตั‚ ัะตั€ะฒะตั€ะฐ:%1 %2. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะฟะพะทะถะต. - - + + Please try to login again: %1 ะŸะพะฒั‚ะพั€ะธั‚ะต ะฟะพะฟั‹ั‚ะบัƒ ะฒั…ะพะดะฐ: %1 - + Failed to join room: %1 ะะต ัƒะดะฐะปะพััŒ ะฟั€ะธัะพะตะดะธะฝะธั‚ัŒัั ะบ ะบะพะผะฝะฐั‚ะต: %1 - + You joined the room ะ’ั‹ ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ ะบ ะบะพะผะฝะฐั‚ะต @@ -283,7 +285,7 @@ ะะต ัƒะดะฐะปะพััŒ ะพั‚ะผะตะฝะธั‚ัŒ ะฟั€ะธะณะปะฐัˆะตะฝะธะต: %1 - + Room creation failed: %1 ะะต ัƒะดะฐะปะพััŒ ัะพะทะดะฐั‚ัŒ ะบะพะผะฝะฐั‚ัƒ: %1 @@ -293,7 +295,7 @@ ะะต ัƒะดะฐะปะพััŒ ะฟะพะบะธะฝัƒั‚ัŒ ะบะพะผะฝะฐั‚ัƒ: %1 - + Failed to kick %1 from %2: %3 ะะต ัƒะดะฐะปะพััŒ ะฒั‹ะณะฝะฐั‚ัŒ %1 ะธะท %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets ะ ะฐััˆะธั„ั€ะพะฒะฐั‚ัŒ ัะตะบั€ะตั‚ั‹ @@ -362,12 +364,12 @@ ะ’ะฒะตะดะธั‚ะต ัะฒะพะน ะบะปัŽั‡ ะฒะพััั‚ะฐะฝะพะฒะปะตะฝะธั ะธะปะธ ะฟะฐั€ะพะปัŒ ะดะปั ั€ะฐััˆะธั„ั€ะพะฒะบะธ ัะตะบั€ะตั‚ะพะฒ: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: ะ’ะฒะตะดะธั‚ะต ัะฒะพะน ะบะปัŽั‡ ะฒะพััั‚ะฐะฝะพะฒะปะตะฝะธั ะธะปะธ ะฟะฐั€ะพะปัŒ ะฝะฐะทะฒะฐะฝะฝั‹ะน %1 ะดะปั ั€ะฐััˆะธั„ั€ะพะฒะบะธ ะ’ะฐัˆะธั… ัะตะบั€ะตั‚ะพะฒ: - + Decryption failed ะ ะฐััˆะธั„ั€ะพะฒะบะฐ ะฝะต ัƒะดะฐะปะฐััŒ @@ -431,7 +433,7 @@ ะŸะพะธัะบ - + People ะ›ัŽะดะธ @@ -494,6 +496,49 @@ ะžะฝะธ ัะพะฒะฟะฐะดะฐัŽั‚! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,53 +558,8 @@ - Encrypted by an unverified device - ะ—ะฐัˆะธั„ั€ะพะฒะฐะฝะพ ะฝะตะฒะตั€ะธั„ะธั†ะธั€ะพะฒะฐะฝั‹ะผ ัƒัั‚ั€ะพะนัั‚ะฒะฐ - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- ะ—ะฐัˆะธั„ั€ะพะฒะฐะฝะฝะพะต ัะพะฑั‹ั‚ะธะต (ะะต ะฝะฐะนะดะตะฝะพ ะบะปัŽั‡ะตะน ะดะปั ะดะตัˆะธั„ั€ะพะฒะฐะฝะธั) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - -- ะ—ะฐัˆะธั„ั€ะพะฒะฐะฝะฝะพะต ัะพะฑั‹ั‚ะธะต(ะะต ะฝะฐะนะดะตะฝะพ ะบะปัŽั‡ะตะน ะดะปั ะดะตัˆะธั„ั€ะพะฒะฐะฝะธั) -- - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- ะžัˆะธะฑะบะฐ ะดะตัˆะธั„ั€ะพะฒะฐะฝะธั (ะะต ัƒะดะฐะปะพััŒ ะฟะพะปัƒั‡ะธั‚ัŒ megolm-ะบะปัŽั‡ะธ ะธะท ะฑะด) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- ะžัˆะธะฑะบะฐ ะ”ะตัˆะธั„ั€ะพะฒะฐะฝะธั (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- ะจะธั„ั€ะพะฒะฐะฝะพะต ะกะพะฑั‹ั‚ะธะต (ะะตะธะทะฒะตัั‚ะฝั‹ะน ั‚ะธะฟ ัะพะฑั‹ั‚ะธั) -- - - - - -- Replay attack! This message index was reused! -- - -- ะั‚ะฐะบะฐ ะฟะพะฒั‚ะพั€ะพะผ! ะ˜ะฝะดะตะบั ัั‚ะพะณะพ ัะพะพะฑั‰ะตะฝะธะต ะฑั‹ะป ะธัะฟะพะปัŒะทะพะฒะฐะฝ ัะฝะพะฒะฐ! -- - - - - -- Message by unverified device! -- - -- ะกะพะพะฑั‰ะตะฝะธะต ะพั‚ ะฝะตะฒะตั€ะธั„ะธั†ะธั€ะพะฒะฐะฝะฝะพะณะพ ัƒัั‚ั€ะพะนัั‚ะฒะฐ! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. + @@ -581,17 +581,26 @@ - Device verification timed out. ะ’ั€ะตะผั ะดะปั ะฒะตั€ะธั„ะธะบะฐั†ะธะธ ัƒัั‚ั€ะพะนัั‚ะฐ ะทะฐะบะพะฝั‡ะธะปะพััŒ. - + Other party canceled the verification. ะ”ั€ัƒะณะฐั ัั‚ะพั€ะพะฝะฐ ะพั‚ะผะตะฝะธะปะฐ ะฒะตั€ะธั„ะธะบะฐั†ะธัŽ. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close ะ—ะฐะบั€ั‹ั‚ัŒ @@ -604,6 +613,81 @@ ะŸะตั€ะตัะปะฐั‚ัŒ ะกะพะพะฑั‰ะตะฝะธะต + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ + + + Close ะ—ะฐะบั€ั‹ั‚ัŒ @@ -645,7 +744,7 @@ InputBar - + Select a file ะ’ั‹ะฑะตั€ะธั‚ะต ั„ะฐะนะป @@ -655,7 +754,7 @@ ะ’ัะต ั„ะฐะนะปั‹ (*) - + Failed to upload media. Please try again. ะะต ัƒะดะฐะปะพััŒ ะทะฐะณั€ัƒะทะธั‚ัŒ ะผะตะดะธะฐ. ะŸะพะถะฐะปัƒะนัั‚ะฐ ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะตั‰ั‘ ั€ะฐะท @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + ะ˜ะดะตะฝั‚ะธั„ะธะบะฐั‚ะพั€ ะธะปะธ ะฟัะตะฒะดะพะฝะธะผ ะบะพะผะฝะฐั‚ั‹ + + + + LeaveRoomDialog + + + Leave room + ะŸะพะบะธะฝัƒั‚ัŒ ะบะพะผะฝะฐั‚ัƒ + + + + Are you sure you want to leave? + ะ’ั‹ ะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝะพ ะถะตะปะฐะตั‚ะต ะฒั‹ะนั‚ะธ? + + LoginPage @@ -760,25 +885,25 @@ Example: https://server.my:8787 ะ’ะžะ™ะขะ˜ - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org ะ’ั‹ ะฒะฒะตะปะธ ะฝะต ะฟั€ะฐะฒะธะปัŒะฝั‹ะน Matrix ID, @joe:matrix.org - + Autodiscovery failed. Received malformed response. ะะฒั‚ะพะพะฑะฝะพั€ัƒะถะตะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ. ะŸะพะปัƒั‡ะตะฝ ะฟะพะฒั€ะตะถะดะตะฝะฝั‹ะน ะพั‚ะฒะตั‚. - + Autodiscovery failed. Unknown error when requesting .well-known. ะะฒั‚ะพะพะฑะฝะพั€ัƒะถะตะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ. ะะต ะธะทะฒะตัั‚ะฐะฝั ะพัˆะธะฑะบะฐ ะฒะพ ะฒั€ะตะผั ะทะฐะฟั€ะพัะฐ .well-known. - + The required endpoints were not found. Possibly not a Matrix server. ะะตะพะฑั…ะพะดะธะผั‹ะต ะบะพะฝะตั‡ะฝั‹ะต ั‚ะพั‡ะบะธ ะฝะต ะฝะฐะนะดะตะฝั‹. ะ’ะพะทะผะพะถะฝะพ, ัั‚ะพ ะฝะต ัะตั€ะฒะตั€ Matrix. @@ -788,30 +913,48 @@ Example: https://server.my:8787 ะŸะพะปัƒั‡ะตะฝ ะฝะตะฒะตั€ะฝั‹ะน ะพั‚ะฒะตั‚. ะฃะฑะตะดะธั‚ะตััŒ, ั‡ั‚ะพ ะดะพะผะตะฝ homeserver ะดะตะนัั‚ะฒะธั‚ะตะปะตะฝ. - + An unknown error occured. Make sure the homeserver domain is valid. ะŸั€ะพะธะทะพัˆะปะฐ ะฝะตะธะทะฒะตัั‚ะฝะฐั ะพัˆะธะฑะบะฐ. ะฃะฑะตะดะธั‚ะตััŒ, ั‡ั‚ะพ ะดะพะผะตะฝ homeserver ะดะตะนัั‚ะฒะธั‚ะตะปะตะฝ. - + SSO LOGIN SSO ะ’ะฅะžะ” - + Empty password ะŸัƒัั‚ะพะน ะฟะฐั€ะพะปัŒ - + SSO login failed SSO ะฒั…ะพะด ะฝะต ัƒะดะฐะปัั + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed ัƒะฑั€ะฐะฝะพ @@ -822,7 +965,7 @@ Example: https://server.my:8787 ะจะธั„ั€ะพะฒะฐะฝะธะต ะฒะบะปัŽั‡ะตะฝะพ - + room name changed to: %1 ะธะผั ะบะพะผะฝะฐั‚ั‹ ะธะทะผะตะฝะตะฝะพ ะฝะฐ: %1 @@ -881,6 +1024,11 @@ Example: https://server.my:8787 Negotiating call... ะกะพะฒะตั€ัˆะตะฝะธะต ะทะฒะพะฝะบะฐ... + + + Allow them in + + MessageInput @@ -905,7 +1053,7 @@ Example: https://server.my:8787 ะะฐะฟะธัะฐั‚ัŒ ัะพะพะฑั‰ะตะฝะธะตโ€ฆ - + Stickers @@ -928,7 +1076,7 @@ Example: https://server.my:8787 MessageView - + Edit ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ @@ -948,17 +1096,19 @@ Example: https://server.my:8787 ะžะฟั†ะธะธ - + + &Copy - + + Copy &link location - + Re&act @@ -1017,6 +1167,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1031,7 +1186,12 @@ Example: https://server.my:8787 ะŸะพะปัƒั‡ะตะฝ ะ—ะฐะฟั€ะพั ะ’ะตั€ะธั„ะธะบะฐั†ะธะธ - + + 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.) + + + + 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? @@ -1076,33 +1236,29 @@ Example: https://server.my:8787 ะŸั€ะธะฝัั‚ัŒ + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 ะพั‚ะฟั€ะฐะฒะธะป ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝะพะต ัะพะพะฑั‰ะตะฝะธะต - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,7 +1289,7 @@ Example: https://server.my:8787 ะœะธะบั€ะพั„ะพะฝ ะฝะต ะฝะฐะนะดะตะฝ. - + Voice ะ“ะพะปะพั @@ -1164,7 +1320,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. ะกะพะทะดะฐั‚ัŒ ัƒะฝะธะบะฐะปัŒะฝั‹ะน ะฟั€ะพั„ะธะปัŒ, ะบะพั‚ะพั€ั‹ะน ะฟะพะทะฒะพะปัะตั‚ ะฒะตัั‚ะธ ะฝะตัะบะพะปัŒะบะพ ะฐะบะบะฐัƒะฝั‚ะพะฒ ะธ ะทะฐะฟัƒัะบะฐั‚ัŒ ะผะฝะพะถะตัั‚ะฒะพ ััƒั‰ะฝะพัั‚ะตะน nheko. @@ -1179,21 +1335,37 @@ Example: https://server.my:8787 ะธะผั ะฟั€ะพั„ะธะปั + + ReadReceipts + + + Read receipts + ะŸั€ะพัะผะพั‚ั€ ะฟะพะปัƒั‡ะฐั‚ะตะปะตะน + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username ะ˜ะผั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. ะ˜ะผั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะฝะต ะดะพะปะถะฝะพ ะฑั‹ั‚ัŒ ะฟัƒัั‚ั‹ะผ ะธ ะดะพะปะถะฝะพ ัะพะดะตั€ะถะฐั‚ัŒ ั‚ะพะปัŒะบะพ ัะธะผะฒะพะปั‹ a-z, 0-9, ., _, =, -, ะธ /. - + Password ะŸะฐั€ะพะปัŒ @@ -1213,7 +1385,7 @@ Example: https://server.my:8787 ะ”ะพะผะฐัˆะฝะธะน ัะตั€ะฒะตั€ - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. ะกะตั€ะฒะตั€ ั€ะฐะทั€ะตัˆะฐัŽั‰ะธะน ั€ะตะณะธัั‚ั€ะฐั†ะธัŽ.ะŸะพัะบะพะปัŒะบัƒ matrix ะดะตั†ะตะฝั‚ั€ะฐะปะธะทะพะฒะฐะฝะฝั‹ะน, ะฝัƒะถะฝะพ ะฒั‹ะฑั€ะฐั‚ัŒ ัะตั€ะฒะตั€ ะณะดะต ะฒั‹ ะผะพะถะตั‚ะต ะทะฐั€ะตะณะธัั‚ั€ะธั€ะพะฒะฐั‚ัŒัั ะธะปะธ ะฟะพะดะฝะธะผะธั‚ะต ัะฒะพะน ัะตั€ะฒะตั€. @@ -1223,27 +1395,17 @@ Example: https://server.my:8787 ะ ะ•ะ“ะ˜ะกะขะ ะะฆะ˜ะฏ - - No supported registration flows! - ะะตั‚ ะฟะพะดะดั€ะตะถะธะฒะฐะตะผั‹ั… ั€ะตะณะธัั‚ั€ะฐั†ะธะพะฝะฝั‹ั… ะฟะพั‚ะพะบะพะฒ - - - - One or more fields have invalid inputs. Please correct those issues and try again. - ะžะดะฝะพ ะธะปะธ ะฑะพะปะตะต ะฟะพะปะตะน ะธะผะตัŽั‚ ะฝะตะบะพั€ั€ะตะบั‚ะฝั‹ะน ะฒะฒะพะด. ะŸะพะถะฐะปัƒะนัั‚ะฐ ัƒัั‚ั€ะฐะฝะธั‚ะต ะพัˆะธะฑะบะธ ะธ ะฟะพะฟั€ะพะฑัƒะนั‚ะต ัะฝะพะฒะฐ. - - - + Autodiscovery failed. Received malformed response. ะะฒั‚ะพะพะฑะฝะพั€ัƒะถะตะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ. ะŸะพะปัƒั‡ะตะฝ ะฟะพะฒั€ะตะถะดะตะฝะฝั‹ะน ะพั‚ะฒะตั‚. - + Autodiscovery failed. Unknown error when requesting .well-known. ะะฒั‚ะพะพะฑะฝะพั€ัƒะถะตะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ. ะะต ะธะทะฒะตัั‚ะฐะฝั ะพัˆะธะฑะบะฐ ะฒะพ ะฒั€ะตะผั ะทะฐะฟั€ะพัะฐ .well-known. - + The required endpoints were not found. Possibly not a Matrix server. ะะตะพะฑั…ะพะดะธะผั‹ะต ะบะพะฝะตั‡ะฝั‹ะต ั‚ะพั‡ะบะธ ะฝะต ะฝะฐะนะดะตะฝั‹. ะ’ะพะทะผะพะถะฝะพ, ัั‚ะพ ะฝะต ัะตั€ะฒะตั€ Matrix. @@ -1258,17 +1420,17 @@ Example: https://server.my:8787 ะŸั€ะพะธะทะพัˆะปะฐ ะฝะตะธะทะฒะตัั‚ะฝะฐั ะพัˆะธะฑะบะฐ. ะฃะฑะตะดะธั‚ะตััŒ, ั‡ั‚ะพ ะดะพะผะตะฝ homeserver ะดะตะนัั‚ะฒะธั‚ะตะปะตะฝ. - + Password is not long enough (min 8 chars) ะกะปะธัˆะบะพะผ ะบะพั€ะพั‚ะบะธะน ะฟะฐั€ะพะปัŒ (ะผะธะฝะธะผัƒะผ 8 ัะธะผะฒะพะปะพะฒ) - + Passwords don't match ะŸะฐั€ะพะปะธ ะฝะต ัะพะฒะฟะฐะดะฐัŽั‚ - + Invalid server name ะะตะฒะตั€ะฝะพะต ะธะผั ัะตั€ะฒะตั€ะฐ @@ -1276,7 +1438,7 @@ Example: https://server.my:8787 ReplyPopup - + Close ะ—ะฐะบั€ั‹ั‚ัŒ @@ -1286,10 +1448,28 @@ Example: https://server.my:8787 ะžั‚ะผะตะฝะธั‚ัŒ ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธะต + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored ะฝะตั‚ ัะพั…ั€ะฐะฝะตะฝะฝะพะน ะฒะตั€ัะธะธ @@ -1297,7 +1477,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1306,16 +1486,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1347,7 +1517,7 @@ Example: https://server.my:8787 - + Status Message @@ -1367,12 +1537,35 @@ Example: https://server.my:8787 - + Logout ะ’ั‹ะนั‚ะธ - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + ะ—ะฐะบั€ั‹ั‚ัŒ + + + Start a new chat ะะฐั‡ะฐั‚ัŒ ะฝะพะฒั‹ะน ั‡ะฐั‚ @@ -1392,7 +1585,7 @@ Example: https://server.my:8787 ะšะฐั‚ะฐะปะพะณ ะบะพะผะฝะฐั‚ - + User settings ะŸะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะธะต ะฝะฐัั‚ั€ะพะนะบะธ @@ -1400,12 +1593,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1419,16 +1612,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings ะะฐัั‚ั€ะพะนะบะธ ะบะพะผะฝะฐั‚ั‹ - + %1 member(s) %1 ัƒั‡ะฐัั‚ะฝะธะบ(ะพะฒ) @@ -1458,7 +1671,12 @@ Example: https://server.my:8787 ะ’ัะต ัะพะพะฑั‰ะตะฝะธั - + + Room access + + + + Anyone and guests ะšะฐะถะดั‹ะน ะธ ะณะพัั‚ะธ @@ -1473,7 +1691,17 @@ Example: https://server.my:8787 ะŸั€ะธะณะปะฐัˆั‘ะฝะฝั‹ะต ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะธ - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption ะจะธั„ั€ะพะฒะฐะฝะธะต @@ -1519,12 +1747,12 @@ Example: https://server.my:8787 ะ’ะตั€ัะธั ะšะพะผะฝะฐั‚ั‹ - + Failed to enable encryption: %1 ะะต ัƒะดะฐะปะพััŒ ะฒะบะปัŽั‡ะธั‚ัŒ ัˆะธั„ั€ะพะฒะฐะฝะธะต: %1 - + Select an avatar ะ’ั‹ะฑะตั€ะธั‚ะต ะฐะฒะฐั‚ะฐั€ @@ -1544,8 +1772,8 @@ Example: https://server.my:8787 ะžัˆะธะฑะบะฐ ะฒะพ ะฒั€ะตะผั ะฟั€ะพั‡ั‚ะตะฝะธั ั„ะฐะนะปะฐ: %1 - - + + Failed to upload image: %s ะะต ัƒะดะฐะปะพััŒ ะทะฐะณั€ัƒะทะธั‚ัŒ ะธะทะพะฑั€ะฐะถะตะฝะธะต: %s @@ -1553,21 +1781,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1622,6 +1878,121 @@ Example: https://server.my:8787 ะžั‚ะผะตะฝะฐ + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1674,18 +2045,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 ะžัˆะธะฑะบะฐ ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธั ัะพะพะฑั‰ะตะฝะธั: %1 - + Failed to encrypt event, sending aborted! ะะต ัƒะดะฐะปะพััŒ ะทะฐัˆะธั„ั€ะพะฒะฐั‚ัŒ ัะพะพะฑั‰ะตะฝะธะต, ะพั‚ะฟั€ะฐะฒะบะฐ ะพั‚ะผะตะฝะตะฝะฐ! - + Save image ะกะพั…ั€ะฐะฝะธั‚ัŒ ะธะทะพะฑั€ะฐะถะตะฝะธะต @@ -1705,7 +2076,7 @@ Example: https://server.my:8787 ะกะพั…ั€ะฐะฝะธั‚ัŒ ั„ะฐะนะป - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1715,7 +2086,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. %1 ัะดะตะปะฐะป ะบะพะผะฝะฐั‚ัƒ ะฟัƒะฑะปะธั‡ะฝะพะน. @@ -1725,7 +2096,17 @@ Example: https://server.my:8787 %1 ัะดะตะปะฐะป ะฒั…ะพะด ะฒ ะบะพะผะฝะฐั‚ัƒ ะฟะพ ะฟั€ะธะณะปะฐัˆะตะฝะธัŽ. - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. %1 ัะดะตะปะฐะป ะบะพะผะฝะฐั‚ัƒ ะพั‚ะบั€ั‹ั‚ะพะน ะดะปั ะณะพัั‚ะตะน. @@ -1745,12 +2126,12 @@ Example: https://server.my:8787 %1 ัะดะตะปะฐะป ะธัั‚ะพั€ะธัŽ ัะพะพะฑั‰ะตะฝะธะน ะฒะธะดะธะผะพ ะดะปั ัƒั‡ะฐัั‚ะฝะธะบะพะฒ ั ัั‚ะพะณะพ ะผะพะผะตะฝั‚ะฐ. - + %1 set the room history visible to members since they were invited. %1 ัะดะตะปะฐะป ะธัั‚ะพั€ะธัŽ ัะพะพะฑั‰ะตะฝะธะน ะฒะธะดะธะผะพะน ะดะปั ัƒั‡ะฐัั‚ะฝะธะบะพะฒ, ั ะผะพะผะตะฝั‚ะฐ ะธั… ะฟั€ะธะณะปะฐัˆะตะฝะธั. - + %1 set the room history visible to members since they joined the room. %1 ัะดะตะปะฐะป ะธัั‚ะพั€ะธัŽ ัะพะพะฑั‰ะตะฝะธะน ะฒะธะดะธะผะพะน ะดะปั ัƒั‡ะฐัั‚ะฝะธะบะพะฒ, ั ะผะพะผะตะฝั‚ะฐ ั‚ะพะณะพ, ะบะฐะบ ะพะฝะธ ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ ะบ ะบะพะผะฝะฐั‚ะต. @@ -1760,12 +2141,12 @@ Example: https://server.my:8787 %1 ะฟะพะผะตะฝัะป ั€ะฐะทั€ะตัˆะตะฝะธั ะดะปั ะบะพะผะฝะฐั‚ั‹. - + %1 was invited. %1 ะฑั‹ะป ะฟั€ะธะณะปะฐัˆะตะฝ. - + %1 changed their avatar. %1 ะฟะพะผะตะฝัะป ัะฒะพะน ะฐะฒะฐั‚ะฐั€. @@ -1775,12 +2156,17 @@ Example: https://server.my:8787 %1 ะฟะพะผะตะฝัะป ะธะฝั„ะพั€ะผะฐั†ะธัŽ ะฒ ะฟั€ะพั„ะธะปะต. - + %1 joined. %1 ะฟั€ะธัะพะตะดะธะฝะธะปัั. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. %1 ะพั‚ะบะปะพะฝะธะป ะฟั€ะธะณะปะฐัˆะตะฝะธะต. @@ -1810,32 +2196,32 @@ Example: https://server.my:8787 %1 ะฑั‹ะป ะทะฐะฑะปะพะบะธั€ะพะฒะฐะฝ. - + Reason: %1 - + %1 redacted their knock. %1 ะพั‚ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะป ะตะณะพ "ัั‚ัƒะบ". - + You joined this room. ะ’ั‹ ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ ะบ ัั‚ะพะน ะบะพะผะฝะฐั‚ะต. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. ะžั‚ะฒะตั€ะณ "ัั‚ัƒะบ" ะพั‚ %1 @@ -1854,7 +2240,7 @@ Example: https://server.my:8787 TimelineRow - + Edited ะ˜ะทะผะตะฝะตะฝะพ @@ -1862,12 +2248,17 @@ Example: https://server.my:8787 TimelineView - + No room open ะšะพะผะฝะฐั‚ะฐ ะฝะต ะฒั‹ะฑั€ะฐะฝะฐ - + + No preview available + + + + %1 member(s) %1 ัƒั‡ะฐัั‚ะฝะธะบ(ะพะฒ) @@ -1892,28 +2283,40 @@ Example: https://server.my:8787 ะ’ะตั€ะฝัƒั‚ัŒัั ะบ ัะฟะธัะบัƒ ะบะพะผะฝะฐั‚ - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - ะะต ะฝะฐะนะดะตะฝะพ ะปะธั‡ะฝะพะณะพ ั‡ะฐั‚ะฐ ั ัั‚ะธะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะผ. ะกะพะทะดะฐะนั‚ะต ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝั‹ะน ะปะธั‡ะฝั‹ะน ั‡ะฐั‚ ั ัั‚ะธะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะผ ะธ ะฟะพะฟั‹ั‚ะฐะนั‚ะตััŒ ะตั‰ะต ั€ะฐะท. - - TopBar - + Back to room list ะ’ะตั€ะฝัƒั‚ัŒัั ะบ ัะฟะธัะบัƒ ะบะพะผะฝะฐั‚ - + No room selected ะšะพะผะฝะฐั‚ั‹ ะฝะต ะฒั‹ะฑั€ะฐะฝั‹ - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options ะะฐัั‚ั€ะพะนะบะธ ะบะพะผะฝะฐั‚ั‹ @@ -1951,10 +2354,35 @@ Example: https://server.my:8787 ะ’ั‹ะนั‚ะธ + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile ะ“ะปะพะฑะฐะปัŒะฝั‹ะน ะŸะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะธะน ะŸั€ะพั„ะธะปัŒ @@ -1964,33 +2392,98 @@ Example: https://server.my:8787 ะŸะพะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะธะน ะŸั€ะพั„ะธะปัŒ ะฒ ะšะพะผะฝะฐั‚ะต - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify ะ’ะตั€ะธั„ะธั†ะธั€ะพะฒะฐั‚ัŒ - - Ban the user - ะ—ะฐะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั - - - - Start a private chat - ะกะพะทะดะฐั‚ัŒ ะฟั€ะธะฒะฐั‚ะฝั‹ะน ั‡ะฐั‚ + + Start a private chat. + - Kick the user - ะ’ั‹ะณะฝะฐั‚ัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั + Kick the user. + - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify ะžั‚ะผะตะฝะธั‚ัŒ ะ’ะตั€ะธั„ะธะบะฐั†ะธัŽ - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar ะ’ั‹ะฑะตั€ะธั‚ะต ะฐะฒะฐั‚ะฐั€ @@ -2013,8 +2506,8 @@ Example: https://server.my:8787 UserSettings - - + + Default ะŸะพ ัƒะผะพะปั‡ะฐะฝะธัŽ @@ -2022,7 +2515,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray ะกะฒะพั€ะฐั‡ะธะฒะฐั‚ัŒ ะฒ ัะธัั‚ะตะผะฝัƒัŽ ะฟะฐะฝะตะปัŒ @@ -2032,22 +2525,22 @@ Example: https://server.my:8787 ะ—ะฐะฟัƒัะบะฐั‚ัŒ ะฒ ัะธัั‚ะตะผะฝะพะน ะฟะฐะฝะตะปะธ - + Group's sidebar ะ‘ะพะบะพะฒะฐั ะฟะฐะฝะตะปัŒ ะณั€ัƒะฟะฟ - + Circular Avatars ะžะบั€ัƒะณะปั‹ะน ะะฒะฐั‚ะฐั€ - + profile: %1 ะฟั€ะพั„ะธะปัŒ: %1 - + Default ะŸะพ ัƒะผะพะปั‡ะฐะฝะธัŽ @@ -2072,7 +2565,7 @@ Example: https://server.my:8787 ะกะšะะงะะขะฌ - + Keep the application running in the background after closing the client window. ะ”ะตั€ะถะฐั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธะต ะทะฐะฟัƒั‰ะตะฝะฝั‹ะผ ะฒ ั„ะพะฝะต, ะฟะพัะปะต ะทะฐะบั€ั‹ั‚ะธั ะพะบะฝะฐ. @@ -2087,6 +2580,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. ะŸะพะผะตะฝัั‚ัŒ ะพั‚ะพะฑั€ะฐะถะตะฝะธะต ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะพะณะพ ะฐะฒะฐั‚ะฐั€ะฐ ะฒ ั‡ะฐั‚ะฐั…. ะ’ะซะšะ› - ะบะฒะฐะดั€ะฐั‚ะฝั‹ะน, ะ’ะšะ› - ะพะบั€ัƒะณะปั‹ะน. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2115,7 +2618,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2172,7 +2675,7 @@ If this is on, rooms which have active notifications (the small circle with a nu ะ•ัะปะธ ัั‚ะพ ะฒะบะปัŽั‡ะตะฝะพ, ะบะพะผะฝะฐั‚ั‹ ะฒ ะบะพั‚ะพั€ั‹ั… ะฒะบะปัŽั‡ะตะฝั‹ ัƒะฒะตะดะพะผะปะตะฝะธั (ะผะฐะปะตะฝัŒะบะธะต ะบั€ัƒะถะบะธ ั ั‡ะธัะปะฐะผะธ) ะฑัƒะดัƒ ะพั‚ัะพั€ั‚ะธั€ะพะฒะฐะฝั‹ ะฝะฐ ะฒะตั€ั…ัƒ. ะšะพะผะฝะฐั‚ั‹, ะบะพั‚ะพั€ั‹ะต ะฒั‹ ะทะฐะณะปัƒัˆะธะปะธ, ะฑัƒะดัƒั‚ ะพั‚ัะพั€ั‚ะธั€ะพะฒะฐะฝั‹ ะฟะพ ะฒั€ะตะผะตะฝะธ, ะฟะพะบะฐ ะฒั‹ ะฝะต ัะดะตะปะฐะตั‚ะต ะธั… ะฒะฐะถะฝะตะต ั‡ะตะผ ะดั€ัƒะณะธะต ะบะพะผะฝะฐั‚ั‹. - + Read receipts ะŸั€ะพัะผะพั‚ั€ ะฟะพะปัƒั‡ะฐั‚ะตะปะตะน @@ -2184,7 +2687,7 @@ Status is displayed next to timestamps. ะกั‚ะฐั‚ัƒ ะพั‚ะพะฑั€ะฐะถะฐะตั‚ัั ะทะฐ ะฒั€ะตะผะตะฝะตะผ ัะพะพะฑั‰ะตะฝะธั. - + Send messages as Markdown ะŸะพัั‹ะปะฐั‚ัŒ ัะพะพะฑั‰ะตะฝะธะต ะฒ ั„ะพั€ะผะฐั‚ะต Markdown @@ -2197,6 +2700,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications ะฃะฒะตะดะพะผะปะตะฝะธั ะฝะฐ ั€ะฐะฑะพั‡ะตะผ ัั‚ะพะปะต @@ -2238,12 +2746,47 @@ This usually causes the application icon in the task bar to animate in some fash ะ”ะตะปะฐั‚ัŒ ัˆั€ะธั„ั‚ ะฑะพะปัŒัˆะต, ะตัะปะธ ัะพะพะฑั‰ะตะฝะธั ัะพะดะตั€ะถะฐั‚ัŒ ั‚ะพะปัŒะบะพ ะฝะตัะบะพะปัŒะบะพ ัะผะพะดะถะธ. - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices ะ”ะตะปะธั‚ัŒัั ะบะปัŽั‡ะฐะผะธ ั ะฟั€ะพะฒะตั€ะตะฝะฝั‹ะผะธ ัƒั‡ะฐัั‚ะฝะธะบะฐะผะธ ะธ ัƒัั‚ั€ะพะนัั‚ะฒะฐะผะธ - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED ะ—ะฐะบะตัˆะธั€ะพะฒะฐะฝะพ @@ -2253,7 +2796,7 @@ This usually causes the application icon in the task bar to animate in some fash ะะ• ะ—ะะšะ•ะจะ˜ะ ะžะ’ะะะž - + Scale factor ะœะฐััˆั‚ะฐะฑ @@ -2328,7 +2871,7 @@ This usually causes the application icon in the task bar to animate in some fash ะžั‚ะฟะตั‡ะฐั‚ะพะบ ัƒัั‚ั€ะพะนัั‚ะฒะฐ - + Session Keys ะšะปัŽั‡ะธ ัะตะฐะฝัะฐ @@ -2348,17 +2891,22 @@ This usually causes the application icon in the task bar to animate in some fash ะจะ˜ะคะ ะžะ’ะะะ˜ะ• - + GENERAL ะ“ะ›ะะ’ะะžะ• - + INTERFACE ะ˜ะะขะ•ะ ะคะ•ะ™ะก - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode ะกะตะฝัะพั€ะฝั‹ะน ั€ะตะถะธะผ @@ -2373,12 +2921,7 @@ This usually causes the application icon in the task bar to animate in some fash ะกะตะผัŒั ัˆั€ะธั„ั‚ะฐ ัะผะพะดะถะธ - - Automatically replies to key requests from other users, if they are verified. - ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะพั‚ะฒะตั‡ะฐั‚ัŒ ะฝะฐ ะทะฐะฟั€ะพัั‹ ะบะปัŽั‡ะตะน ะพั‚ ะดั€ัƒะณะธั… ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน, ะตัะปะธ ะพะฝะธ ะฒะตั€ะธั„ะธั†ะธั€ะพะฒะฐะฝั‹. - - - + Master signing key @@ -2398,7 +2941,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2428,14 +2971,14 @@ This usually causes the application icon in the task bar to animate in some fash ะ’ัะต ั„ะฐะนะปั‹ (*) - + Open Sessions File ะžั‚ะบั€ั‹ั‚ัŒ ั„ะฐะนะป ัะตะฐะฝัะพะฒ - + @@ -2443,20 +2986,20 @@ This usually causes the application icon in the task bar to animate in some fash ะžัˆะธะฑะบะฐ - - + + File Password ะ˜ะปะธ ะฒะฒะตะดะธั‚ะต ะฟะฐั€ะพะปัŒ? ะŸะฐั€ะพะปัŒ ั„ะฐะนะปะฐ - + Enter the passphrase to decrypt the file: ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะพะปัŒะฝัƒัŽ ั„ั€ะฐะทัƒ ะดะปั ั€ะฐััˆะธั„ั€ะพะฒะฐะฝะธั ั„ะฐะนะปะฐ: - + The password cannot be empty ะŸะฐั€ะพะปัŒ ะฝะต ะผะพะถะตั‚ ะฑั‹ั‚ัŒ ะฟัƒัั‚ั‹ะผ @@ -2471,6 +3014,14 @@ This usually causes the application icon in the task bar to animate in some fash ะคะฐะนะป ะดะปั ัะพั…ั€ะฐะฝะตะฝะธั ัะบัะฟะพั€ั‚ะธั€ะพะฒะฐะฝะฝั‹ั… ะบะปัŽั‡ะตะน ัะตะฐะฝัะฐ + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + ะะต ะฝะฐะนะดะตะฝะพ ะปะธั‡ะฝะพะณะพ ั‡ะฐั‚ะฐ ั ัั‚ะธะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะผ. ะกะพะทะดะฐะนั‚ะต ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝั‹ะน ะปะธั‡ะฝั‹ะน ั‡ะฐั‚ ั ัั‚ะธะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะผ ะธ ะฟะพะฟั‹ั‚ะฐะนั‚ะตััŒ ะตั‰ะต ั€ะฐะท. + + Waiting @@ -2596,37 +3147,6 @@ This usually causes the application icon in the task bar to animate in some fash ะ—ะฐะฟัƒัั‚ะธั‚ะต ั€ะตะทะตั€ะฒะฝั‹ะน ะฒะฐั€ะธะฐะฝั‚, ะฟั€ะพะนะดะธั‚ะต ะตะณะพ ัˆะฐะณะธ ะธ ะฟะพะดั‚ะฒะตั€ะดะธั‚ะต ะทะฐะฒะตั€ัˆะตะฝะธะต. - - dialogs::JoinRoom - - - Join - ะŸั€ะธัะพะตะดะธะฝะธั‚ัŒัั - - - - Cancel - ะžั‚ะผะตะฝะฐ - - - - Room ID or alias - ะ˜ะดะตะฝั‚ะธั„ะธะบะฐั‚ะพั€ ะธะปะธ ะฟัะตะฒะดะพะฝะธะผ ะบะพะผะฝะฐั‚ั‹ - - - - dialogs::LeaveRoom - - - Cancel - ะžั‚ะผะตะฝะฐ - - - - Are you sure you want to leave? - ะ’ั‹ ะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝะพ ะถะตะปะฐะตั‚ะต ะฒั‹ะนั‚ะธ? - - dialogs::Logout @@ -2680,32 +3200,6 @@ Media size: %2 ะ ะตัˆะธั‚ะต reCAPTCHA ะธ ะฝะฐะถะผะธั‚ะต ะบะฝะพะฟะบัƒ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั - - dialogs::ReadReceipts - - - Read receipts - ะŸั€ะพัะผะพั‚ั€ะตั‚ัŒ ะฟะพะปัƒั‡ะฐั‚ะตะปะตะน - - - - Close - ะ—ะฐะบั€ั‹ั‚ัŒ - - - - dialogs::ReceiptItem - - - Today %1 - ะกะตะณะพะดะฝั %1 - - - - Yesterday %1 - ะ’ั‡ะตั€ะฐ %1 - - message-description sent: @@ -2719,47 +3213,47 @@ Media size: %2 %1 ะพั‚ะฟั€ะฐะฒะธะป ะฐัƒะดะธะพ ะทะฐะฟะธััŒ - + You sent an image ะ’ั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ะบะฐั€ั‚ะธะฝะบัƒ - + %1 sent an image %1 ะพั‚ะฟั€ะฐะฒะธะป ะบะฐั€ั‚ะธะฝะบัƒ - + You sent a file ะ’ั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ั„ะฐะนะป - + %1 sent a file %1 ะพั‚ะฟั€ะฐะฒะธะป ั„ะฐะนะป - + You sent a video ะ’ั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ะฒะธะดะตะพ - + %1 sent a video %1 ะพั‚ะฟั€ะฐะฒะธะป ะฒะธะดะตะพ - + You sent a sticker ะ’ั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ัั‚ะธะบะตั€ - + %1 sent a sticker %1 ะพั‚ะฟั€ะฐะฒะธะป ัั‚ะธะบะตั€ - + You sent a notification ะ’ั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ะพะฟะพะฒะตั‰ะตะฝะธะต @@ -2774,7 +3268,7 @@ Media size: %2 ะ’ั‹: %1 - + %1: %2 %1: %2 @@ -2794,27 +3288,27 @@ Media size: %2 ะ’ั‹ ะฝะฐั‡ะฐะปะธ ะทะฒะพะฝะพะบ - + %1 placed a call %1 ะฝะฐั‡ะฐะป ะทะฒะพะฝะพะบ - + You answered a call ะ’ั‹ ะพั‚ะฒะตั‚ะธะปะธ ะฝะฐ ะทะฒะพะฝะพะบ - + %1 answered a call %1 ะพั‚ะฒะตั‚ะธะป ะฝะฐ ะทะฒะพะฝะพะบ - + You ended a call ะ’ั‹ ะทะฐะบะพะฝั‡ะธะปะธ ั€ะฐะทะณะพะฒะพั€ - + %1 ended a call %1 ะ—ะฐะบะพะฝั‡ะธะป ั€ะฐะทะณะพะฒะพั€ @@ -2822,7 +3316,7 @@ Media size: %2 utils - + Unknown Message Type ะะตะธะทะฒะตัั‚ะฝั‹ะน ะขะธะฟ ะกะพะพะฑั‰ะตะฝะธั diff --git a/resources/langs/nheko_si.ts b/resources/langs/nheko_si.ts index cf425990..579ca1cf 100644 --- a/resources/langs/nheko_si.ts +++ b/resources/langs/nheko_si.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... @@ -56,7 +56,7 @@ CallInvite - + Video Call @@ -74,7 +74,7 @@ CallInviteBar - + Video Call @@ -117,7 +117,7 @@ CallManager - + Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. - + Confirm invite - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 @@ -182,7 +182,7 @@ - + Kicked user: %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 @@ -227,12 +227,12 @@ - + Do you really want to start a private chat with %1? - + Cache migration failed! @@ -247,33 +247,35 @@ - + Failed to restore OLM account. Please login again. + + Failed to restore save data. Please login again. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. - - + + Please try to login again: %1 - + Failed to join room: %1 - + You joined the room @@ -283,7 +285,7 @@ - + Room creation failed: %1 @@ -293,7 +295,7 @@ - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close @@ -645,7 +744,7 @@ InputBar - + Select a file @@ -655,7 +754,7 @@ - + Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + + + + + LeaveRoomDialog + + + Leave room + + + + + Are you sure you want to leave? + + + LoginPage @@ -756,25 +881,25 @@ Example: https://server.my:8787 - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -784,30 +909,48 @@ Example: https://server.my:8787 - + An unknown error occured. Make sure the homeserver domain is valid. - + SSO LOGIN - + Empty password - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed @@ -818,7 +961,7 @@ Example: https://server.my:8787 - + room name changed to: %1 @@ -877,6 +1020,11 @@ Example: https://server.my:8787 Negotiating call... + + + Allow them in + + MessageInput @@ -901,7 +1049,7 @@ Example: https://server.my:8787 - + Stickers @@ -924,7 +1072,7 @@ Example: https://server.my:8787 MessageView - + Edit @@ -944,17 +1092,19 @@ Example: https://server.my:8787 - + + &Copy - + + Copy &link location - + Re&act @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1072,33 +1232,29 @@ Example: https://server.my:8787 + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 - + Password is not long enough (min 8 chars) - + Passwords don't match - + Invalid server name @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + + + + Start a new chat @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 - + User settings @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1414,16 +1607,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1453,7 +1666,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1468,7 +1686,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1514,12 +1742,12 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 - + Select an avatar @@ -1539,8 +1767,8 @@ Example: https://server.my:8787 - - + + Failed to upload image: %s @@ -1548,21 +1776,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1617,6 +1873,121 @@ Example: https://server.my:8787 + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1669,18 +2040,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 - + Failed to encrypt event, sending aborted! - + Save image @@ -1700,7 +2071,7 @@ Example: https://server.my:8787 - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1709,7 +2080,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1719,7 +2090,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1739,12 +2120,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1754,12 +2135,12 @@ Example: https://server.my:8787 - + %1 was invited. - + %1 changed their avatar. @@ -1769,12 +2150,17 @@ Example: https://server.my:8787 - + %1 joined. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. @@ -1804,32 +2190,32 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. - + You joined this room. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. @@ -1848,7 +2234,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1856,12 +2242,17 @@ Example: https://server.my:8787 TimelineView - + No room open - + + No preview available + + + + %1 member(s) @@ -1886,28 +2277,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options @@ -1945,10 +2348,35 @@ Example: https://server.my:8787 + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1958,33 +2386,98 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar @@ -2007,8 +2500,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2016,7 +2509,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray @@ -2026,22 +2519,22 @@ Example: https://server.my:8787 - + Group's sidebar - + Circular Avatars - + profile: %1 - + Default @@ -2066,7 +2559,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2081,6 +2574,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2109,7 +2612,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2164,7 +2667,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts @@ -2175,7 +2678,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown @@ -2187,6 +2690,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications @@ -2227,12 +2735,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2242,7 +2785,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2317,7 +2860,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -2337,17 +2880,22 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2362,12 +2910,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2387,7 +2930,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2417,14 +2960,14 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File - + @@ -2432,19 +2975,19 @@ This usually causes the application icon in the task bar to animate in some fash - - + + File Password - + Enter the passphrase to decrypt the file: - + The password cannot be empty @@ -2459,6 +3002,14 @@ This usually causes the application icon in the task bar to animate in some fash + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2584,37 +3135,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - - - - - Room ID or alias - - - - - dialogs::LeaveRoom - - - Cancel - - - - - Are you sure you want to leave? - - - dialogs::Logout @@ -2666,32 +3186,6 @@ Media size: %2 - - dialogs::ReadReceipts - - - Read receipts - - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2705,47 +3199,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2760,7 +3254,7 @@ Media size: %2 - + %1: %2 @@ -2780,27 +3274,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2808,7 +3302,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/langs/nheko_sv.ts b/resources/langs/nheko_sv.ts index 25db1c4b..e19136db 100644 --- a/resources/langs/nheko_sv.ts +++ b/resources/langs/nheko_sv.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... Ringer upp... @@ -56,7 +56,7 @@ CallInvite - + Video Call Videosamtal @@ -74,7 +74,7 @@ CallInviteBar - + Video Call Videosamtal @@ -117,7 +117,7 @@ CallManager - + Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 Kunde inte bjuda in anvรคndare: %1 - + Invited user: %1 Bjรถd in anvรคndare: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. Kunde inte migrera cachen till den nuvarande versionen. Detta kan bero pรฅ flera anledningar, vรคnligen rapportera problemet och prova en รคldre version under tiden. Du kan ocksรฅ fรถrsรถka att manuellt radera cachen. - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. Rum %1 skapat. - + Confirm invite Bekrรคfta inbjudan - + Do you really want to invite %1 (%2)? ร„r du sรคker pรฅ att du vill bjuda in %1 (%2)? - + Failed to invite %1 to %2: %3 Kunde inte bjuda in %1 till %2: %3 @@ -182,7 +182,7 @@ ร„r du sรคker pรฅ att du vill sparka ut %1 (%2)? - + Kicked user: %1 Sparkade ut anvรคndare: %1 @@ -197,7 +197,7 @@ ร„r du sรคker pรฅ att du vill bannlysa %1 (%2)? - + Failed to ban %1 in %2: %3 Kunde inte bannlysa %1 i %2: %3 @@ -217,7 +217,7 @@ ร„r du sรคker pรฅ att du vill hรคva bannlysningen av %1 (%2)? - + Failed to unban %1 in %2: %3 Kunde inte hรคva bannlysningen av %1 i %2: %3 @@ -227,12 +227,12 @@ Hรคvde bannlysningen av anvรคndare: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! Cache-migration misslyckades! @@ -247,33 +247,35 @@ Cachen pรฅ ditt lagringsmedia รคr nyare รคn vad denna version av Nheko stรถdjer. Vรคnligen uppdatera eller rensa din cache. - + Failed to restore OLM account. Please login again. Kunde inte รฅterstรคlla OLM-konto. Vรคnligen logga in pรฅ nytt. + + Failed to restore save data. Please login again. Kunde inte รฅterstรคlla sparad data. Vรคnligen logga in pรฅ nytt. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Kunde inte sรคtta upp krypteringsnycklar. Svar frรฅn servern: %1 %2. Vรคnligen fรถrsรถk igen senare. - - + + Please try to login again: %1 Vรคnligen fรถrsรถk logga in pรฅ nytt: %1 - + Failed to join room: %1 Kunde inte gรฅ med i rum: %1 - + You joined the room Du gick med i rummet @@ -283,7 +285,7 @@ Kunde inte ta bort inbjudan: %1 - + Room creation failed: %1 Kunde inte skapa rum: %1 @@ -293,7 +295,7 @@ Kunde inte lรคmna rum: %1 - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets Dekryptera hemliga nycklar @@ -362,12 +364,12 @@ Ange din รฅterstรคllningsnyckel eller lรถsenfras fรถr att dekryptera dina hemliga nycklar: - + Enter your recovery key or passphrase called %1 to decrypt your secrets: Ange din รฅterstรคllningsnyckel eller lรถsenfras vid namn %1 fรถr att dekryptera dina hemliga nycklar: - + Decryption failed Dekryptering misslyckades @@ -431,7 +433,7 @@ Sรถk - + People Personer @@ -494,6 +496,49 @@ De รถverensstรคmmer! + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,55 +558,10 @@ - Encrypted by an unverified device + Encrypted by an unverified device or the key is from an untrusted source like the key backup. - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - -- Krypterat Event (Inga nycklar kunde hittas fรถr dekryptering) -- - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - -- Dekrypteringsfel (Kunde inte hรคmta megolm-nycklar frรฅn databas) -- - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - -- Dekrypteringsfel (%1) -- - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - -- Krypterat Event (Okรคnd eventtyp) -- - - - - -- Replay attack! This message index was reused! -- - -- Replay-attack! Detta meddelandeindex har blivit รฅteranvรคnt! -- - - - - -- Message by unverified device! -- - -- Meddelande frรฅn overifierad enhet! -- - - Failed @@ -581,17 +581,26 @@ - Device verification timed out. Enhetsverifikation tog fรถr lรฅng tid. - + Other party canceled the verification. Motparten avbrรถt verifikationen. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close Stรคng @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + Avbryt + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close Stรคng @@ -645,7 +744,7 @@ InputBar - + Select a file Vรคlj en fil @@ -655,7 +754,7 @@ Alla Filer (*) - + Failed to upload media. Please try again. Kunde inte ladda upp media. Vรคnligen fรถrsรถk igen. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ Avbryt + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + Rum-ID eller alias + + + + LeaveRoomDialog + + + Leave room + Lรคmna rum + + + + Are you sure you want to leave? + ร„r du sรคker pรฅ att du vill lรคmna? + + LoginPage @@ -760,25 +885,25 @@ Exempel: https://server.my:8787 INLOGGNING - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. Autouppslag misslyckades. Mottog felkonstruerat svar. - + Autodiscovery failed. Unknown error when requesting .well-known. Autouppslag misslyckades. Okรคnt fel uppstod vid begรคran av .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Kunde inte hitta de nรถdvรคndiga รคndpunkterna. Mรถjligtvis inte en Matrix-server. @@ -788,35 +913,53 @@ Exempel: https://server.my:8787 Mottog felkonstruerat svar. Se till att hemserver-domรคnen รคr giltig. - + An unknown error occured. Make sure the homeserver domain is valid. Ett okรคnt fel uppstod. Se till att hemserver-domรคnen รคr giltig. - + SSO LOGIN SSO INLOGGNING - + Empty password Tomt lรถsenord - + SSO login failed SSO-inloggning misslyckades + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + Encryption enabled Kryptering aktiverad - + room name changed to: %1 rummets namn รคndrat till: %1 @@ -866,18 +1009,23 @@ Exempel: https://server.my:8787 Fรถrhandlar samtalโ€ฆ - + + Allow them in + + + + %1 answered the call. %1 besvarade samtalet. - + removed borttagen - + %1 ended the call. %1 avslutade samtalet. @@ -905,7 +1053,7 @@ Exempel: https://server.my:8787 Skriv ett meddelandeโ€ฆ - + Stickers @@ -928,7 +1076,7 @@ Exempel: https://server.my:8787 MessageView - + Edit @@ -948,17 +1096,19 @@ Exempel: https://server.my:8787 Alternativ - + + &Copy - + + Copy &link location - + Re&act @@ -1017,6 +1167,11 @@ Exempel: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1031,7 +1186,12 @@ Exempel: https://server.my:8787 Mottog Verifikationsfรถrfrรฅgan - + + 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.) + + + + 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? Du kan verifiera dina enheter fรถr att lรฅta andra anvรคndare se vilka av dem som faktiskt tillhรถr dig. Detta gรถr รคven att nyckelbackup fungerar automatiskt. Verifiera %1 nu? @@ -1076,33 +1236,29 @@ Exempel: https://server.my:8787 Godkรคnn + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message %1 skickade ett krypterat meddelande - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - %1: %2 - @@ -1133,7 +1289,7 @@ Exempel: https://server.my:8787 Ingen mikrofon kunde hittas. - + Voice Rรถst @@ -1164,7 +1320,7 @@ Exempel: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. Skapa en unik profil, vilket tillรฅter dig att logga in pรฅ flera konton samtidigt och starta flera instanser av Nheko. @@ -1179,21 +1335,37 @@ Exempel: https://server.my:8787 profilnamn + + ReadReceipts + + + Read receipts + Lรคskvitton + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username Anvรคndarnamn - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. Anvรคndarnamnet kan inte vara tomt, och mรฅste enbart innehรฅlla tecknen a-z, 0-9, ., _, =, -, och /. - + Password Lรถsenord @@ -1213,7 +1385,7 @@ Exempel: https://server.my:8787 Hemserver - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. En server som tillรฅter registrering. Eftersom matrix รคr decentraliserat behรถver du fรถrst hitta en server du kan registrera dig pรฅ, eller upprรคtta en pรฅ egen hand. @@ -1223,27 +1395,17 @@ Exempel: https://server.my:8787 REGISTRERA - - No supported registration flows! - Inga stรถdda registreringsflรถden! - - - - One or more fields have invalid inputs. Please correct those issues and try again. - Ett eller flera fรคlt har ogiltigt innehรฅll. Vรคnligen korrigera problemen och fรถrsรถk igen. - - - + Autodiscovery failed. Received malformed response. Autouppslag misslyckades. Mottog felkonstruerat svar. - + Autodiscovery failed. Unknown error when requesting .well-known. Autouppslag misslyckades. Okรคnt fel uppstod vid begรคran av .well-known. - + The required endpoints were not found. Possibly not a Matrix server. Kunde inte hitta de nรถdvรคndiga รคndpunkterna. Mรถjligtvis inte en Matrix-server. @@ -1258,17 +1420,17 @@ Exempel: https://server.my:8787 Ett okรคnt fel uppstod. Se till att hemserver-domรคnen รคr giltig. - + Password is not long enough (min 8 chars) Lรถsenordet รคr inte lรฅngt nog (minst 8 tecken) - + Passwords don't match Lรถsenorden stรคmmer inte รถverens - + Invalid server name Ogiltigt servernamn @@ -1276,7 +1438,7 @@ Exempel: https://server.my:8787 ReplyPopup - + Close Stรคng @@ -1286,10 +1448,28 @@ Exempel: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored ingen version lagrad @@ -1297,7 +1477,7 @@ Exempel: https://server.my:8787 RoomList - + New tag @@ -1306,16 +1486,6 @@ Exempel: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1347,7 +1517,7 @@ Exempel: https://server.my:8787 - + Status Message @@ -1367,12 +1537,35 @@ Exempel: https://server.my:8787 - + Logout Logga ut - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + Stรคng + + + Start a new chat Starta en ny chatt @@ -1392,7 +1585,7 @@ Exempel: https://server.my:8787 Rumkatalog - + User settings Anvรคndarinstรคllningar @@ -1400,12 +1593,12 @@ Exempel: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1418,16 +1611,36 @@ Exempel: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1457,7 +1670,12 @@ Exempel: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1472,7 +1690,17 @@ Exempel: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1518,12 +1746,12 @@ Exempel: https://server.my:8787 - + Failed to enable encryption: %1 Kunde inte aktivera kryptering: %1 - + Select an avatar Vรคlj en avatar @@ -1543,8 +1771,8 @@ Exempel: https://server.my:8787 Kunde inte lรคsa filen: %1 - - + + Failed to upload image: %s Kunde inte ladda upp bilden: %s @@ -1552,21 +1780,49 @@ Exempel: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1621,6 +1877,121 @@ Exempel: https://server.my:8787 Avbryt + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1673,18 +2044,18 @@ Exempel: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Kunde inte maskera meddelande: %1 - + Failed to encrypt event, sending aborted! Kunde inte kryptera event, sรคndning avbruten! - + Save image Spara bild @@ -1704,7 +2075,7 @@ Exempel: https://server.my:8787 Spara fil - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1713,7 +2084,7 @@ Exempel: https://server.my:8787 - + %1 opened the room to the public. %1 รถppnade rummet till allmรคnheten. @@ -1723,7 +2094,17 @@ Exempel: https://server.my:8787 %1 satte rummet till att krรคva inbjudan fรถr att gรฅ med. - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. %1 รถppnade rummet fรถr gรคstรฅtkomst. @@ -1743,12 +2124,12 @@ Exempel: https://server.my:8787 %1 gjorde rummets historik lรคslig fรถr medlemmar frรฅn och med nu. - + %1 set the room history visible to members since they were invited. %1 gjorde rummets historik lรคslig fรถr medlemmar sedan de blev inbjudna. - + %1 set the room history visible to members since they joined the room. %1 gjorde rummets historik lรคslig fรถr medlemmar sedan de gick med i rummet. @@ -1758,12 +2139,12 @@ Exempel: https://server.my:8787 %1 har รคndrat rummets behรถrigheter. - + %1 was invited. %1 blev inbjuden. - + %1 changed their avatar. %1 รคndrade sin avatar. @@ -1773,12 +2154,17 @@ Exempel: https://server.my:8787 %1 รคndrade nรฅgon profilinfo. - + %1 joined. %1 gick med. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. %1 avvisade sin inbjudan. @@ -1808,32 +2194,32 @@ Exempel: https://server.my:8787 %1 blev bannlyst. - + Reason: %1 - + %1 redacted their knock. %1 maskerade sin knackning. - + You joined this room. Du gick med i detta rum. - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. Avvisade knackningen frรฅn %1. @@ -1852,7 +2238,7 @@ Exempel: https://server.my:8787 TimelineRow - + Edited @@ -1860,12 +2246,17 @@ Exempel: https://server.my:8787 TimelineView - + No room open Inget rum รถppet - + + No preview available + + + + %1 member(s) @@ -1890,28 +2281,40 @@ Exempel: https://server.my:8787 Tillbaka till rumlista - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - Ingen krypterad privat chatt med denna anvรคndare kunde hittas. Skapa en krypterad privat chatt med anvรคndaren och fรถrsรถk igen. - - TopBar - + Back to room list Tillbaka till rumlista - + No room selected Inget rum markerat - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options Alternativ fรถr rum @@ -1949,10 +2352,35 @@ Exempel: https://server.my:8787 Avsluta + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1962,33 +2390,98 @@ Exempel: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify Bekrรคfta - - Ban the user - Bannlys anvรคndaren - - - - Start a private chat - Starta en privat chatt + + Start a private chat. + - Kick the user - Sparka ut anvรคndaren + Kick the user. + - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar Vรคlj en avatar @@ -2011,8 +2504,8 @@ Exempel: https://server.my:8787 UserSettings - - + + Default @@ -2020,7 +2513,7 @@ Exempel: https://server.my:8787 UserSettingsPage - + Minimize to tray Minimera till systemtrรฅg @@ -2030,22 +2523,22 @@ Exempel: https://server.my:8787 Starta i systemtrรฅg - + Group's sidebar Gruppens sidofรคlt - + Circular Avatars Cirkulรคra avatarer - + profile: %1 profil: %1 - + Default @@ -2070,7 +2563,7 @@ Exempel: https://server.my:8787 LADDA NED - + Keep the application running in the background after closing the client window. Hรฅll applikationen igรฅng i bakgrunden efter att ha stรคngt klienten. @@ -2086,6 +2579,16 @@ OFF - square, ON - Circle. ร„ndra utseendet av anvรคndaravatarer i chattar. AV - Kvadrat, Pร… - Cirkel. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2115,7 +2618,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2173,7 +2676,7 @@ Om denna instรคllning รคr av kommer listan รถver rum endast sorteras efter nรคr Om denna instรคllning รคr pรฅ kommer rum med aktiva notifikationer (den lilla cirkeln med ett nummer i) sorteras hรถgst upp. Rum som du stรคngt av notifikationer fรถr kommer fortfarande sorteras efter nรคr det sista meddelandet skickades, eftersom du inte verkar tycka att de รคr lika viktiga som andra rum. - + Read receipts Lรคskvitton @@ -2185,7 +2688,7 @@ Status is displayed next to timestamps. Status visas bredvid tidsstรคmpel. - + Send messages as Markdown Skicka meddelanden som Markdown @@ -2198,6 +2701,11 @@ Om denna instรคllning รคr av kommer alla meddelanden skickas som oformatterad te + Play animated images only on hover + + + + Desktop notifications Skrivbordsnotifikationer @@ -2239,12 +2747,47 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< ร–ka fontstorleken pรฅ meddelanden som enbart innehรฅller ett par emoji. - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices Dela nycklar med verifierade anvรคndare och enheter - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED SPARAD @@ -2254,7 +2797,7 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< EJ SPARAD - + Scale factor Storleksfaktor @@ -2329,7 +2872,7 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< Enhetsfingeravtryck - + Session Keys Sessionsnycklar @@ -2349,17 +2892,22 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< KRYPTERING - + GENERAL ALLMร„NT - + INTERFACE GRร„NSSNITT - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode Touchskรคrmslรคge @@ -2374,12 +2922,7 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< Emoji font-familj - - Automatically replies to key requests from other users, if they are verified. - Svarar automatiskt pรฅ nyckelfรถrfrรฅgningar frรฅn andra anvรคndare om de รคr verifierade. - - - + Master signing key Primรคr signeringsnyckel @@ -2399,7 +2942,7 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< Nyckeln fรถr att verifiera andra anvรคndare. Om den รคr sparad lokalt, kommer alla enheter tillhรถrande en anvรคndare verifieras nรคr anvรคndaren verifieras. - + Self signing key Sjรคlvsigneringsnyckel @@ -2429,14 +2972,14 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< Alla Filer (*) - + Open Sessions File ร–ppna sessionsfil - + @@ -2444,19 +2987,19 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< Fel - - + + File Password Fillรถsenord - + Enter the passphrase to decrypt the file: Ange lรถsenfrasen fรถr att dekryptera filen: - + The password cannot be empty Lรถsenordet kan inte vara tomt @@ -2471,6 +3014,14 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< Fil fรถr att spara de exporterade sessionsnycklarna + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + Ingen krypterad privat chatt med denna anvรคndare kunde hittas. Skapa en krypterad privat chatt med anvรคndaren och fรถrsรถk igen. + + Waiting @@ -2596,37 +3147,6 @@ Detta gรถr vanligtvis att ikonen i aktivitetsfรคltet animeras pรฅ nรฅgot sรคtt.< ร–ppna reserven, fรถlj stegen och bekrรคfta nรคr du slutfรถrt dem. - - dialogs::JoinRoom - - - Join - Gรฅ med - - - - Cancel - Avbryt - - - - Room ID or alias - Rum-ID eller alias - - - - dialogs::LeaveRoom - - - Cancel - Avbryt - - - - Are you sure you want to leave? - ร„r du sรคker pรฅ att du vill lรคmna? - - dialogs::Logout @@ -2680,32 +3200,6 @@ Mediastorlek: %2 Lรถs reCAPTCHAn och tryck pรฅ Bekrรคfta - - dialogs::ReadReceipts - - - Read receipts - Lรคskvitton - - - - Close - Stรคng - - - - dialogs::ReceiptItem - - - Today %1 - Idag %1 - - - - Yesterday %1 - Igรฅr %1 - - message-description sent: @@ -2719,47 +3213,47 @@ Mediastorlek: %2 %1 skickade ett ljudklipp - + You sent an image Du skickade en bild - + %1 sent an image %1 skickade en bild - + You sent a file Du skickade en fil - + %1 sent a file %1 skickade en fil - + You sent a video Du skickade en video - + %1 sent a video %1 skickade en video - + You sent a sticker Du skickade en sticker - + %1 sent a sticker %1 skickade en sticker - + You sent a notification Du skickade en notis @@ -2774,7 +3268,7 @@ Mediastorlek: %2 Du: %1 - + %1: %2 %1: %2 @@ -2794,27 +3288,27 @@ Mediastorlek: %2 Du ringde upp - + %1 placed a call %1 ringde upp - + You answered a call Du besvarade ett samtal - + %1 answered a call %1 besvarade ett samtal - + You ended a call Du lade pรฅ - + %1 ended a call %1 lade pรฅ @@ -2822,7 +3316,7 @@ Mediastorlek: %2 utils - + Unknown Message Type Okรคnd meddelandetyp diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index 75e9db95..36cfb0a0 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -4,7 +4,7 @@ ActiveCallBar - + Calling... @@ -56,7 +56,7 @@ CallInvite - + Video Call @@ -74,7 +74,7 @@ CallInviteBar - + Video Call @@ -117,7 +117,7 @@ CallManager - + Entire screen @@ -125,23 +125,23 @@ ChatPage - + Failed to invite user: %1 ้‚€่ฏท็”จๆˆทๅคฑ่ดฅ: %1 - + Invited user: %1 ้‚€่ฏทๅทฒๅ‘้€: %1 - + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. ๆ— ๆณ•่ฟ็งป็ผ“ๅญ˜ๅˆฐ็›ฎๅ‰็‰ˆๆœฌ๏ผŒๅฏ่ƒฝๆœ‰ๅคš็งๅŽŸๅ› ๅผ•ๅ‘ๆญค็ฑป้—ฎ้ข˜ใ€‚ๆ‚จๅฏไปฅๆ–ฐๅปบไธ€ไธช่ฎฎ้ข˜ๅนถ็ปง็ปญไฝฟ็”จไน‹ๅ‰็‰ˆๆœฌ๏ผŒๆˆ–่€…ๆ‚จๅฏไปฅๅฐ่ฏ•ๆ‰‹ๅŠจๅˆ ้™ค็ผ“ๅญ˜ใ€‚ - + Confirm join @@ -151,23 +151,23 @@ - + Room %1 created. ๆˆฟ้—ดโ€œ%1โ€ๅทฒๅˆ›ๅปบ - + Confirm invite - + Do you really want to invite %1 (%2)? - + Failed to invite %1 to %2: %3 @@ -182,7 +182,7 @@ - + Kicked user: %1 ่ธขๅ‡บ็”จๆˆท: %1 @@ -197,7 +197,7 @@ - + Failed to ban %1 in %2: %3 @@ -217,7 +217,7 @@ - + Failed to unban %1 in %2: %3 @@ -227,12 +227,12 @@ ่งฃ็ฆ็”จๆˆท: %1 - + Do you really want to start a private chat with %1? - + Cache migration failed! ็ผ“ๅญ˜่ฟ็งปๅคฑ่ดฅ๏ผ @@ -247,33 +247,35 @@ ๆœฌๅœฐ็ผ“ๅญ˜็‰ˆๆœฌๆฏ”็Žฐ็”จ็š„Nheko็‰ˆๆœฌๆ–ฐใ€‚่ฏทๅ‡็บงNhekoๆˆ–ๆ‰‹ๅŠจๆธ…้™ค็ผ“ๅญ˜ใ€‚ - + Failed to restore OLM account. Please login again. ๆขๅค OLM ่ดฆๆˆทๅคฑ่ดฅใ€‚่ฏท้‡ๆ–ฐ็™ปๅฝ•ใ€‚ + + Failed to restore save data. Please login again. ๆขๅคไฟๅญ˜็š„ๆ•ฐๆฎๅคฑ่ดฅใ€‚่ฏท้‡ๆ–ฐ็™ปๅฝ•ใ€‚ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. ่ฎพ็ฝฎๅฏ†้’ฅๅคฑ่ดฅใ€‚ๆœๅŠกๅ™จ่ฟ”ๅ›žไฟกๆฏ: %1 %2ใ€‚่ฏท็จๅŽๅ†่ฏ•ใ€‚ - - + + Please try to login again: %1 ่ฏทๅฐ่ฏ•ๅ†ๆฌก็™ปๅฝ•๏ผš%1 - + Failed to join room: %1 ๆ— ๆณ•ๅŠ ๅ…ฅๆˆฟ้—ด: %1 - + You joined the room ๆ‚จๅทฒๅŠ ๅ…ฅๆˆฟ้—ด @@ -283,7 +285,7 @@ ๆ— ๆณ•็งป้™ค้‚€่ฏท: %1 - + Room creation failed: %1 ๅˆ›ๅปบ่Šๅคฉๅฎคๅคฑ่ดฅ๏ผš%1 @@ -293,7 +295,7 @@ ็ฆปๅผ€่Šๅคฉๅฎคๅคฑ่ดฅ๏ผš%1 - + Failed to kick %1 from %2: %3 @@ -352,7 +354,7 @@ CrossSigningSecrets - + Decrypt secrets @@ -362,12 +364,12 @@ - + Enter your recovery key or passphrase called %1 to decrypt your secrets: - + Decryption failed @@ -431,7 +433,7 @@ - + People @@ -494,6 +496,49 @@ + + Encrypted + + + There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient. + + + + + This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message. + + + + + There was an internal error reading the decryption key from the database. + + + + + There was an error decrypting this message. + + + + + The message couldn't be parsed. + + + + + The encryption key was reused! Someone is possibly trying to insert false messages into this chat! + + + + + Unknown decryption error + + + + + Request key + + + EncryptionIndicator @@ -513,52 +558,7 @@ - Encrypted by an unverified device - - - - - EventStore - - - -- Encrypted Event (No keys found for decryption) -- - Placeholder, when the message was not decrypted yet or can't be decrypted. - - - - - -- Encrypted Event (Key not valid for this index) -- - Placeholder, when the message can't be decrypted with this key since it is not valid for this index - - - - - - -- Decryption Error (failed to retrieve megolm keys from db) -- - Placeholder, when the message can't be decrypted, because the DB access failed. - - - - - - -- Decryption Error (%1) -- - Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. - - - - - -- Encrypted Event (Unknown event type) -- - Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. - - - - - -- Replay attack! This message index was reused! -- - - - - - -- Message by unverified device! -- + Encrypted by an unverified device or the key is from an untrusted source like the key backup. @@ -581,17 +581,26 @@ - Device verification timed out. - + Other party canceled the verification. - + + Verification messages received out of order! + + + + + Unknown verification error. + + + + Close @@ -604,6 +613,81 @@ + + ImagePackEditorDialog + + + Editing image pack + + + + + Add images + + + + + Stickers (*.png *.webp *.gif *.jpg *.jpeg) + + + + + State key + + + + + Packname + + + + + Attribution + + + + + + Use as Emoji + + + + + + Use as Sticker + + + + + Shortcode + + + + + Body + + + + + Remove from pack + + + + + Remove + + + + + Cancel + ๅ–ๆถˆ + + + + Save + + + ImagePackSettingsDialog @@ -612,7 +696,17 @@ - + + Create account pack + + + + + New room pack + + + + Private pack @@ -627,7 +721,7 @@ - + Enable globally @@ -637,7 +731,12 @@ - + + Edit + + + + Close @@ -645,7 +744,7 @@ InputBar - + Select a file ้€‰ๆ‹ฉไธ€ไธชๆ–‡ไปถ @@ -655,7 +754,7 @@ ๆ‰€ๆœ‰ๆ–‡ไปถ๏ผˆ*๏ผ‰ - + Failed to upload media. Please try again. @@ -663,7 +762,7 @@ InviteDialog - + Invite users to %1 @@ -694,6 +793,32 @@ ๅ–ๆถˆ + + JoinRoomDialog + + + Join room + + + + + Room ID or alias + ่Šๅคฉๅฎค ID ๆˆ–ๅˆซๅ + + + + LeaveRoomDialog + + + Leave room + ็ฆปๅผ€่Šๅคฉๅฎค + + + + Are you sure you want to leave? + ไฝ ็กฎๅฎš่ฆ็ฆปๅผ€ๅ—๏ผŸ + + LoginPage @@ -756,25 +881,25 @@ Example: https://server.my:8787 ็™ปๅฝ• - + - + You have entered an invalid Matrix ID e.g @joe:matrix.org - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. ๆฒกๆ‰พๅˆฐ่ฆๆฑ‚็š„็ปˆ็ซฏใ€‚ๅฏ่ƒฝไธๆ˜ฏไธ€ไธช Matrix ๆœๅŠกๅ™จใ€‚ @@ -784,30 +909,48 @@ Example: https://server.my:8787 ๆ”ถๅˆฐๅฝขๅผ้”™่ฏฏ็š„ๅ“ๅบ”ใ€‚่ฏท็กฎ่ฎคๆœๅŠกๅ™จๅŸŸๅๅˆๆณ•ใ€‚ - + An unknown error occured. Make sure the homeserver domain is valid. ๅ‘็”Ÿไบ†ไธ€ไธชๆœช็Ÿฅ้”™่ฏฏใ€‚่ฏท็กฎ่ฎคๆœๅŠกๅ™จๅŸŸๅๅˆๆณ•ใ€‚ - + SSO LOGIN - + Empty password ็ฉบๅฏ†็  - + SSO login failed + + LogoutDialog + + + Log out + + + + + A call is in progress. Log out? + + + + + Are you sure you want to log out? + + + MessageDelegate - + removed @@ -818,7 +961,7 @@ Example: https://server.my:8787 - + room name changed to: %1 @@ -877,6 +1020,11 @@ Example: https://server.my:8787 Negotiating call... + + + Allow them in + + MessageInput @@ -901,7 +1049,7 @@ Example: https://server.my:8787 ๅ†™ไธ€ๆกๆถˆๆฏโ€ฆ - + Stickers @@ -924,7 +1072,7 @@ Example: https://server.my:8787 MessageView - + Edit @@ -944,17 +1092,19 @@ Example: https://server.my:8787 - + + &Copy - + + Copy &link location - + Re&act @@ -1013,6 +1163,11 @@ Example: https://server.my:8787 Copy link to eve&nt + + + &Go to quoted message + + NewVerificationRequest @@ -1027,7 +1182,12 @@ Example: https://server.my:8787 - + + 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.) + + + + 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? @@ -1072,33 +1232,29 @@ Example: https://server.my:8787 ๆŽฅๅ— + + NotificationWarning + + + You are about to notify the whole room + + + NotificationsManager - - + + %1 sent an encrypted message - - * %1 %2 - Format an emote message in a notification, %1 is the sender, %2 the message - - - - + %1 replied: %2 Format a reply in a notification. %1 is the sender, %2 the message - - - %1: %2 - Format a normal message in a notification. %1 is the sender, %2 the message - - @@ -1129,7 +1285,7 @@ Example: https://server.my:8787 - + Voice @@ -1160,7 +1316,7 @@ Example: https://server.my:8787 QCoreApplication - + Create a unique profile, which allows you to log into several accounts at the same time and start multiple instances of nheko. @@ -1175,21 +1331,37 @@ Example: https://server.my:8787 + + ReadReceipts + + + Read receipts + ้˜…่ฏปๅ›žๆ‰ง + + + + ReadReceiptsModel + + + Yesterday, %1 + + + RegisterPage - + Username ็”จๆˆทๅ - + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. - + Password ๅฏ†็  @@ -1209,7 +1381,7 @@ Example: https://server.my:8787 - + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. @@ -1219,27 +1391,17 @@ Example: https://server.my:8787 ๆณจๅ†Œ - - No supported registration flows! - - - - - One or more fields have invalid inputs. Please correct those issues and try again. - - - - + Autodiscovery failed. Received malformed response. - + Autodiscovery failed. Unknown error when requesting .well-known. - + The required endpoints were not found. Possibly not a Matrix server. ๆฒกๆ‰พๅˆฐ่ฆๆฑ‚็š„็ปˆ็ซฏใ€‚ๅฏ่ƒฝไธๆ˜ฏไธ€ไธช Matrix ๆœๅŠกๅ™จใ€‚ @@ -1254,17 +1416,17 @@ Example: https://server.my:8787 ๅ‘็”Ÿไบ†ไธ€ไธชๆœช็Ÿฅ้”™่ฏฏใ€‚่ฏท็กฎ่ฎคๆœๅŠกๅ™จๅŸŸๅๅˆๆณ•ใ€‚ - + Password is not long enough (min 8 chars) ๅฏ†็ ไธๅคŸ้•ฟ๏ผˆ่‡ณๅฐ‘8ไธชๅญ—็ฌฆ๏ผ‰ - + Passwords don't match ๅฏ†็ ไธๅŒน้… - + Invalid server name ๆ— ๆ•ˆ็š„ๆœๅŠกๅ™จๅ @@ -1272,7 +1434,7 @@ Example: https://server.my:8787 ReplyPopup - + Close @@ -1282,10 +1444,28 @@ Example: https://server.my:8787 + + RoomDirectory + + + Explore Public Rooms + + + + + Search for public rooms + + + + + Choose custom homeserver + + + RoomInfo - + no version stored @@ -1293,7 +1473,7 @@ Example: https://server.my:8787 RoomList - + New tag @@ -1302,16 +1482,6 @@ Example: https://server.my:8787 Enter the tag you want to use: - - - Leave Room - - - - - Are you sure you want to leave this room? - - Leave room @@ -1343,7 +1513,7 @@ Example: https://server.my:8787 - + Status Message @@ -1363,12 +1533,35 @@ Example: https://server.my:8787 - + Logout ็™ปๅ‡บ - + + Encryption not set up + Cross-signing setup has not run yet. + + + + + Unverified login + The user just signed in with this device and hasn't verified their master key. + + + + + Please verify your other devices + There are unverified devices signed in to this account. + + + + + Close + + + + Start a new chat ๅผ€ๅง‹ๆ–ฐ็š„่Šๅคฉ @@ -1388,7 +1581,7 @@ Example: https://server.my:8787 ่Šๅคฉๅฎค็›ฎๅฝ• - + User settings ็”จๆˆท่ฎพ็ฝฎ @@ -1396,12 +1589,12 @@ Example: https://server.my:8787 RoomMembers - + Members of %1 - + %n people in %1 Summary above list of members @@ -1413,16 +1606,36 @@ Example: https://server.my:8787 Invite more people + + + This room is not encrypted! + + + + + This user is verified. + + + + + This user isn't verified, but is still using the same master key from the first time you met. + + + + + This user has unverified devices! + + RoomSettings - + Room Settings - + %1 member(s) @@ -1452,7 +1665,12 @@ Example: https://server.my:8787 - + + Room access + + + + Anyone and guests @@ -1467,7 +1685,17 @@ Example: https://server.my:8787 - + + By knocking + + + + + Restricted by membership in other rooms + + + + Encryption @@ -1513,12 +1741,12 @@ Example: https://server.my:8787 - + Failed to enable encryption: %1 ๅฏ็”จๅŠ ๅฏ†ๅคฑ่ดฅ๏ผš%1 - + Select an avatar ้€‰ๆ‹ฉไธ€ไธชๅคดๅƒ @@ -1538,8 +1766,8 @@ Example: https://server.my:8787 - - + + Failed to upload image: %s ไธŠไผ ๅ›พๅƒๅคฑ่ดฅ๏ผš%s @@ -1547,21 +1775,49 @@ Example: https://server.my:8787 RoomlistModel - + Pending invite. - + Previewing this room - + No preview available + + Root + + + Please enter your login password to continue: + + + + + Please enter a valid email address to continue: + + + + + Please enter a valid phone number to continue: + + + + + Please enter the token, which has been sent to you: + + + + + Wait for the confirmation link to arrive, then continue. + + + ScreenShare @@ -1616,6 +1872,121 @@ Example: https://server.my:8787 ๅ–ๆถˆ + + SecretStorage + + + Failed to connect to secret storage + + + + + Nheko could not connect to the secure storage to save encryption secrets to. This can have multiple reasons. Check if your D-Bus service is running and you have configured a service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If you are having trouble, feel free to open an issue here: https://github.com/Nheko-Reborn/nheko/issues + + + + + SelfVerificationCheck + + + 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! + + + + + Encryption setup successfully + + + + + Failed to setup encryption: %1 + + + + + Setup Encryption + + + + + Hello and welcome to Matrix! +It 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! + + + + + Activate Encryption + + + + + 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. +If 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. + + + + + verify + + + + + enter passphrase + + + + + SelfVerificationStatus + + + Failed to create keys for cross-signing! + + + + + Failed to create keys for online key backup! + + + + + Failed to create keys secure server side secret storage! + + + + + Encryption Setup + + + + + Encryption setup failed: %1 + + + + + SingleImagePackModel + + + + Failed to update image pack: %1 + + + + + Failed to delete old image pack: %1 + + + + + Failed to open image: %1 + + + + + Failed to upload image: %1 + + + StatusIndicator @@ -1668,18 +2039,18 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 ๅˆ ้™คๆถˆๆฏๅคฑ่ดฅ๏ผš%1 - + Failed to encrypt event, sending aborted! - + Save image ไฟๅญ˜ๅ›พๅƒ @@ -1699,7 +2070,7 @@ Example: https://server.my:8787 - + %1 and %2 are typing. Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) @@ -1707,7 +2078,7 @@ Example: https://server.my:8787 - + %1 opened the room to the public. @@ -1717,7 +2088,17 @@ Example: https://server.my:8787 - + + %1 allowed to join this room by knocking. + + + + + %1 allowed members of the following rooms to automatically join this room: %2 + + + + %1 made the room open to guests. @@ -1737,12 +2118,12 @@ Example: https://server.my:8787 - + %1 set the room history visible to members since they were invited. - + %1 set the room history visible to members since they joined the room. @@ -1752,12 +2133,12 @@ Example: https://server.my:8787 - + %1 was invited. - + %1 changed their avatar. @@ -1767,12 +2148,17 @@ Example: https://server.my:8787 - + %1 joined. - + + %1 joined via authorisation from %2's server. + + + + %1 rejected their invite. @@ -1802,32 +2188,32 @@ Example: https://server.my:8787 - + Reason: %1 - + %1 redacted their knock. - + You joined this room. ๆ‚จๅทฒๅŠ ๅ…ฅๆญคๆˆฟ้—ด - + %1 has changed their avatar and changed their display name to %2. - + %1 has changed their display name to %2. - + Rejected the knock from %1. @@ -1846,7 +2232,7 @@ Example: https://server.my:8787 TimelineRow - + Edited @@ -1854,12 +2240,17 @@ Example: https://server.my:8787 TimelineView - + No room open - + + No preview available + + + + %1 member(s) @@ -1884,28 +2275,40 @@ Example: https://server.my:8787 - - TimelineViewManager - - - No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. - - - TopBar - + Back to room list - + No room selected - + + This room is not encrypted! + + + + + This room contains only verified devices. + + + + + This room contains verified devices and devices which have never changed their master key. + + + + + This room contains unverified devices! + + + + Room options ่Šๅคฉๅฎค้€‰้กน @@ -1943,10 +2346,35 @@ Example: https://server.my:8787 ้€€ๅ‡บ + + UIA + + + No available registration flows! + + + + + + + Registration aborted + + + + + Please enter a valid registration token. + + + + + Invalid token + + + UserProfile - + Global User Profile @@ -1956,33 +2384,98 @@ Example: https://server.my:8787 - - + + Change avatar globally. + + + + + Change avatar. Will only apply to this room. + + + + + Change display name globally. + + + + + Change display name. Will only apply to this room. + + + + + Room: %1 + + + + + This is a room-specific profile. The user's name and avatar may be different from their global versions. + + + + + Open the global profile for this user. + + + + + Verify - - Ban the user - - - - - Start a private chat + + Start a private chat. - Kick the user + Kick the user. - + + Ban the user. + + + + + Refresh device list. + + + + + Sign out this device. + + + + + Change device name. + + + + + Last seen %1 from %2 + + + + Unverify - + + Sign out device %1 + + + + + You signed out this device. + + + + Select an avatar ้€‰ๆ‹ฉไธ€ไธชๅคดๅƒ @@ -2005,8 +2498,8 @@ Example: https://server.my:8787 UserSettings - - + + Default @@ -2014,7 +2507,7 @@ Example: https://server.my:8787 UserSettingsPage - + Minimize to tray ๆœ€ๅฐๅŒ–่‡ณๆ‰˜็›˜ @@ -2024,22 +2517,22 @@ Example: https://server.my:8787 ๅœจๆ‰˜็›˜ๅฏๅŠจ - + Group's sidebar ็พค็ป„ไพง่พนๆ  - + Circular Avatars - + profile: %1 - + Default @@ -2064,7 +2557,7 @@ Example: https://server.my:8787 - + Keep the application running in the background after closing the client window. @@ -2079,6 +2572,16 @@ Example: https://server.my:8787 OFF - square, ON - Circle. + + + Use identicons + + + + + Display an identicon instead of a letter when a user has not set an avatar. + + Show a column containing groups and tags next to the room list. @@ -2107,7 +2610,7 @@ be blurred. - + Privacy screen timeout (in seconds [0 - 3600]) @@ -2162,7 +2665,7 @@ If this is on, rooms which have active notifications (the small circle with a nu - + Read receipts ้˜…่ฏปๅ›žๆ‰ง @@ -2173,7 +2676,7 @@ Status is displayed next to timestamps. - + Send messages as Markdown @@ -2185,6 +2688,11 @@ When disabled, all messages are sent as a plain text. + Play animated images only on hover + + + + Desktop notifications ๆกŒ้ข้€š็Ÿฅ @@ -2225,12 +2733,47 @@ This usually causes the application icon in the task bar to animate in some fash - + + Send encrypted messages to verified users only + + + + + Requires a user to be verified to send encrypted messages to them. This improves safety but makes E2EE more tedious. + + + + Share keys with verified users and devices - + + Automatically replies to key requests from other users, if they are verified, even if that device shouldn't have access to those keys otherwise. + + + + + Online Key Backup + + + + + Download message encryption keys from and upload to the encrypted online key backup. + + + + + Enable online key backup + + + + + The Nheko authors recommend not enabling online key backup until symmetric online key backup is available. Enable anyway? + + + + CACHED @@ -2240,7 +2783,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -2315,7 +2858,7 @@ This usually causes the application icon in the task bar to animate in some fash ่ฎพๅค‡ๆŒ‡็บน - + Session Keys ไผš่ฏๅฏ†้’ฅ @@ -2335,17 +2878,22 @@ This usually causes the application icon in the task bar to animate in some fash ๅŠ ๅฏ† - + GENERAL ้€š็”จ - + INTERFACE - + + Plays media like GIFs or WEBPs only when explicitly hovering over them. + + + + Touchscreen mode @@ -2360,12 +2908,7 @@ This usually causes the application icon in the task bar to animate in some fash - - Automatically replies to key requests from other users, if they are verified. - - - - + Master signing key @@ -2385,7 +2928,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Self signing key @@ -2415,14 +2958,14 @@ This usually causes the application icon in the task bar to animate in some fash ๆ‰€ๆœ‰ๆ–‡ไปถ๏ผˆ*๏ผ‰ - + Open Sessions File ๆ‰“ๅผ€ไผš่ฏๆ–‡ไปถ - + @@ -2430,19 +2973,19 @@ This usually causes the application icon in the task bar to animate in some fash ้”™่ฏฏ - - + + File Password ๆ–‡ไปถๅฏ†็  - + Enter the passphrase to decrypt the file: ่พ“ๅ…ฅๅฏ†็ ไปฅ่งฃๅฏ†ๆ–‡ไปถ๏ผš - + The password cannot be empty ๅฏ†็ ไธ่ƒฝไธบ็ฉบ @@ -2457,6 +3000,14 @@ This usually causes the application icon in the task bar to animate in some fash ไฟๅญ˜ๅฏผๅ‡บ็š„ไผš่ฏๅฏ†้’ฅ็š„ๆ–‡ไปถ + + VerificationManager + + + No encrypted private chat found with this user. Create an encrypted private chat with this user and try again. + + + Waiting @@ -2582,37 +3133,6 @@ This usually causes the application icon in the task bar to animate in some fash - - dialogs::JoinRoom - - - Join - - - - - Cancel - ๅ–ๆถˆ - - - - Room ID or alias - ่Šๅคฉๅฎค ID ๆˆ–ๅˆซๅ - - - - dialogs::LeaveRoom - - - Cancel - ๅ–ๆถˆ - - - - Are you sure you want to leave? - ไฝ ็กฎๅฎš่ฆ็ฆปๅผ€ๅ—๏ผŸ - - dialogs::Logout @@ -2666,32 +3186,6 @@ Media size: %2 ่งฃๅ†ณ reCAPTCHA ๅนถๆŒ‰็กฎ่ฎคๆŒ‰้’ฎ - - dialogs::ReadReceipts - - - Read receipts - ้˜…่ฏปๅ›žๆ‰ง - - - - Close - - - - - dialogs::ReceiptItem - - - Today %1 - - - - - Yesterday %1 - - - message-description sent: @@ -2705,47 +3199,47 @@ Media size: %2 - + You sent an image - + %1 sent an image - + You sent a file - + %1 sent a file - + You sent a video - + %1 sent a video - + You sent a sticker - + %1 sent a sticker - + You sent a notification @@ -2760,7 +3254,7 @@ Media size: %2 - + %1: %2 @@ -2780,27 +3274,27 @@ Media size: %2 - + %1 placed a call - + You answered a call - + %1 answered a call - + You ended a call - + %1 ended a call @@ -2808,7 +3302,7 @@ Media size: %2 utils - + Unknown Message Type diff --git a/resources/nheko.appdata.xml b/resources/nheko.appdata.xml index 8cd2bfa3..4fe9e722 100644 --- a/resources/nheko.appdata.xml +++ b/resources/nheko.appdata.xml @@ -3,7 +3,7 @@ nheko.desktop CC0-1.0 - GPL-3.0-or-later and CC-BY + GPL-3.0-or-later nheko Desktop client for the Matrix protocol diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 4a9a565c..58b22863 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -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 diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml index e56d7d46..22a04b74 100644 --- a/resources/qml/ChatPage.qml +++ b/resources/qml/ChatPage.qml @@ -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 } } diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml index 491913be..6a2c642c 100644 --- a/resources/qml/CommunitiesList.qml +++ b/resources/qml/CommunitiesList.qml @@ -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 { diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 00fc3216..6bde67fa 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -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)) } diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 52d2eeed..6bc16a18 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -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."); } } diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 26752f92..eccd6ce9 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -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; diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 7fb09684..c95929ce 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -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 { diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index e5c6b4ec..7ed30112 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -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) + } + + } + } diff --git a/resources/qml/NotificationWarning.qml b/resources/qml/NotificationWarning.qml new file mode 100644 index 00000000..75ef5f17 --- /dev/null +++ b/resources/qml/NotificationWarning.qml @@ -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 + } + + } + +} diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml index defcc611..c7141c81 100644 --- a/resources/qml/QuickSwitcher.qml +++ b/resources/qml/QuickSwitcher.qml @@ -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; diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 8fbfce91..db255bd3 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -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 { diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index cc7d32ea..f6b26041 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -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 } diff --git a/resources/qml/SelfVerificationCheck.qml b/resources/qml/SelfVerificationCheck.qml new file mode 100644 index 00000000..23997e58 --- /dev/null +++ b/resources/qml/SelfVerificationCheck.qml @@ -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 + } + +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c8ac6bc7..8214d9de 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -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 diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 7f67c028..e9f482c9 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -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) } } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml deleted file mode 100644 index 767d2317..00000000 --- a/resources/qml/UserProfile.qml +++ /dev/null @@ -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() - } - -} diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml index 36c26a97..853266c6 100644 --- a/resources/qml/components/AvatarListTile.qml +++ b/resources/qml/components/AvatarListTile.qml @@ -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 diff --git a/resources/qml/components/MainWindowDialog.qml b/resources/qml/components/MainWindowDialog.qml new file mode 100644 index 00000000..901260b5 --- /dev/null +++ b/resources/qml/components/MainWindowDialog.qml @@ -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 + } + +} diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml index cd00a9d4..6616d3ce 100644 --- a/resources/qml/delegates/Encrypted.qml +++ b/resources/qml/delegates/Encrypted.qml @@ -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 diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index b432018c..64e365c8 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -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 } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index a8bdf183..9f889106 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -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) + } + } } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index aa7ee530..fbc4a637 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -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 } } } diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 8bbce10e..60154837 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -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 diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 58aa99ca..11ad3aeb 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -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; } " + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").replace("", "")
     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
+    }
+
 }
diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
index 8e0271d6..5bc8b9c8 100644
--- a/resources/qml/device-verification/DeviceVerification.qml
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -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 {
diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml
index 71ef8b9b..6bfd4aed 100644
--- a/resources/qml/device-verification/Failed.qml
+++ b/resources/qml/device-verification/Failed.qml
@@ -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
diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml
index 5ae2d25b..7e521605 100644
--- a/resources/qml/device-verification/NewVerificationRequest.qml
+++ b/resources/qml/device-verification/NewVerificationRequest.qml
@@ -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 {
diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml
index b0f431f6..1db5d45f 100644
--- a/resources/qml/dialogs/ImagePackEditorDialog.qml
+++ b/resources/qml/dialogs/ImagePackEditorDialog.qml
@@ -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
diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml
index 5181619c..e48040c1 100644
--- a/resources/qml/dialogs/ImagePackSettingsDialog.qml
+++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml
@@ -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
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
index e0f17851..12211c60 100644
--- a/resources/qml/dialogs/InputDialog.qml
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -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 {
diff --git a/resources/qml/InviteDialog.qml b/resources/qml/dialogs/InviteDialog.qml
similarity index 98%
rename from resources/qml/InviteDialog.qml
rename to resources/qml/dialogs/InviteDialog.qml
index 2c0e15a7..86c176be 100644
--- a/resources/qml/InviteDialog.qml
+++ b/resources/qml/dialogs/InviteDialog.qml
@@ -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 {
diff --git a/resources/qml/dialogs/JoinRoomDialog.qml b/resources/qml/dialogs/JoinRoomDialog.qml
new file mode 100644
index 00000000..df31d994
--- /dev/null
+++ b/resources/qml/dialogs/JoinRoomDialog.qml
@@ -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
+        }
+
+    }
+
+}
diff --git a/resources/qml/dialogs/LeaveRoomDialog.qml b/resources/qml/dialogs/LeaveRoomDialog.qml
new file mode 100644
index 00000000..e9c12e8f
--- /dev/null
+++ b/resources/qml/dialogs/LeaveRoomDialog.qml
@@ -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)
+}
diff --git a/resources/qml/dialogs/LogoutDialog.qml b/resources/qml/dialogs/LogoutDialog.qml
new file mode 100644
index 00000000..eb82dd15
--- /dev/null
+++ b/resources/qml/dialogs/LogoutDialog.qml
@@ -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()
+}
diff --git a/resources/qml/dialogs/PhoneNumberInputDialog.qml b/resources/qml/dialogs/PhoneNumberInputDialog.qml
new file mode 100644
index 00000000..b4f2a9f0
--- /dev/null
+++ b/resources/qml/dialogs/PhoneNumberInputDialog.qml
@@ -0,0 +1,1744 @@
+// SPDX-FileCopyrightText: 2021 Mirian Margiani
+// 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: inputDialog
+
+    property alias prompt: promptLabel.text
+    property alias echoMode: statusInput.echoMode
+    property var onAccepted: undefined
+
+    modality: Qt.NonModal
+    flags: Qt.Dialog
+    Component.onCompleted: Nheko.reparent(inputDialog)
+    width: 350
+    height: fontMetrics.lineSpacing * 7
+
+    GridLayout {
+        rowSpacing: Nheko.paddingMedium
+        columnSpacing: Nheko.paddingMedium
+        anchors.margins: Nheko.paddingMedium
+        anchors.fill: parent
+        columns: 2
+
+        Label {
+            id: promptLabel
+
+            Layout.columnSpan: 2
+            color: Nheko.colors.text
+        }
+
+        ComboBox {
+            id: numberPrefix
+
+            editable: false
+
+            delegate: ItemDelegate {
+                text: n + " (" + p + ")"
+            }
+            // taken from https://gitlab.com/whisperfish/whisperfish/-/blob/master/qml/js/countries.js
+
+            //n=name,i=ISO,p=prefix -- see countries.js.md for source
+            model: ListModel {
+                ListElement {
+                    n: "Afghanistan"
+                    i: "AF"
+                    p: "+93"
+                }
+
+                ListElement {
+                    n: "ร…land Islands"
+                    i: "AX"
+                    p: "+358 18"
+                }
+
+                ListElement {
+                    n: "Albania"
+                    i: "AL"
+                    p: "+355"
+                }
+
+                ListElement {
+                    n: "Algeria"
+                    i: "DZ"
+                    p: "+213"
+                }
+
+                ListElement {
+                    n: "American Samoa"
+                    i: "AS"
+                    p: "+1 684"
+                }
+
+                ListElement {
+                    n: "Andorra"
+                    i: "AD"
+                    p: "+376"
+                }
+
+                ListElement {
+                    n: "Angola"
+                    i: "AO"
+                    p: "+244"
+                }
+
+                ListElement {
+                    n: "Anguilla"
+                    i: "AI"
+                    p: "+1 264"
+                }
+
+                ListElement {
+                    n: "Antigua and Barbuda"
+                    i: "AG"
+                    p: "+1 268"
+                }
+
+                ListElement {
+                    n: "Argentina"
+                    i: "AR"
+                    p: "+54"
+                }
+
+                ListElement {
+                    n: "Armenia"
+                    i: "AM"
+                    p: "+374"
+                }
+
+                ListElement {
+                    n: "Aruba"
+                    i: "AW"
+                    p: "+297"
+                }
+
+                ListElement {
+                    n: "Ascension"
+                    i: "SH"
+                    p: "+247"
+                }
+
+                ListElement {
+                    n: "Australia"
+                    i: "AU"
+                    p: "+61"
+                }
+
+                ListElement {
+                    n: "Australian Antarctic Territory"
+                    i: "AQ"
+                    p: "+672 1"
+                }
+                //ListElement{n:"Australian External Territories";i:"";p:"+672"} // NO ISO
+
+                ListElement {
+                    n: "Austria"
+                    i: "AT"
+                    p: "+43"
+                }
+
+                ListElement {
+                    n: "Azerbaijan"
+                    i: "AZ"
+                    p: "+994"
+                }
+
+                ListElement {
+                    n: "Bahamas"
+                    i: "BS"
+                    p: "+1 242"
+                }
+
+                ListElement {
+                    n: "Bahrain"
+                    i: "BH"
+                    p: "+973"
+                }
+
+                ListElement {
+                    n: "Bangladesh"
+                    i: "BD"
+                    p: "+880"
+                }
+
+                ListElement {
+                    n: "Barbados"
+                    i: "BB"
+                    p: "+1 246"
+                }
+
+                ListElement {
+                    n: "Barbuda"
+                    i: "AG"
+                    p: "+1 268"
+                }
+
+                ListElement {
+                    n: "Belarus"
+                    i: "BY"
+                    p: "+375"
+                }
+
+                ListElement {
+                    n: "Belgium"
+                    i: "BE"
+                    p: "+32"
+                }
+
+                ListElement {
+                    n: "Belize"
+                    i: "BZ"
+                    p: "+501"
+                }
+
+                ListElement {
+                    n: "Benin"
+                    i: "BJ"
+                    p: "+229"
+                }
+
+                ListElement {
+                    n: "Bermuda"
+                    i: "BM"
+                    p: "+1 441"
+                }
+
+                ListElement {
+                    n: "Bhutan"
+                    i: "BT"
+                    p: "+975"
+                }
+
+                ListElement {
+                    n: "Bolivia"
+                    i: "BO"
+                    p: "+591"
+                }
+
+                ListElement {
+                    n: "Bonaire"
+                    i: "BQ"
+                    p: "+599 7"
+                }
+
+                ListElement {
+                    n: "Bosnia and Herzegovina"
+                    i: "BA"
+                    p: "+387"
+                }
+
+                ListElement {
+                    n: "Botswana"
+                    i: "BW"
+                    p: "+267"
+                }
+
+                ListElement {
+                    n: "Brazil"
+                    i: "BR"
+                    p: "+55"
+                }
+
+                ListElement {
+                    n: "British Indian Ocean Territory"
+                    i: "IO"
+                    p: "+246"
+                }
+
+                ListElement {
+                    n: "Brunei Darussalam"
+                    i: "BN"
+                    p: "+673"
+                }
+
+                ListElement {
+                    n: "Bulgaria"
+                    i: "BG"
+                    p: "+359"
+                }
+
+                ListElement {
+                    n: "Burkina Faso"
+                    i: "BF"
+                    p: "+226"
+                }
+
+                ListElement {
+                    n: "Burundi"
+                    i: "BI"
+                    p: "+257"
+                }
+
+                ListElement {
+                    n: "Cambodia"
+                    i: "KH"
+                    p: "+855"
+                }
+
+                ListElement {
+                    n: "Cameroon"
+                    i: "CM"
+                    p: "+237"
+                }
+
+                ListElement {
+                    n: "Canada"
+                    i: "CA"
+                    p: "+1"
+                }
+
+                ListElement {
+                    n: "Cape Verde"
+                    i: "CV"
+                    p: "+238"
+                }
+                //ListElement{n:"Caribbean Netherlands";i:"";p:"+599 3"} // NO ISO
+
+                //ListElement{n:"Caribbean Netherlands";i:"";p:"+599 4"} // NO ISO
+                //ListElement{n:"Caribbean Netherlands";i:"";p:"+599 7"} // NO ISO
+                ListElement {
+                    n: "Cayman Islands"
+                    i: "KY"
+                    p: "+1 345"
+                }
+
+                ListElement {
+                    n: "Central African Republic"
+                    i: "CF"
+                    p: "+236"
+                }
+
+                ListElement {
+                    n: "Chad"
+                    i: "TD"
+                    p: "+235"
+                }
+
+                ListElement {
+                    n: "Chatham Island (New Zealand)"
+                    i: "NZ"
+                    p: "+64"
+                }
+
+                ListElement {
+                    n: "Chile"
+                    i: "CL"
+                    p: "+56"
+                }
+
+                ListElement {
+                    n: "China"
+                    i: "CN"
+                    p: "+86"
+                }
+
+                ListElement {
+                    n: "Christmas Island"
+                    i: "CX"
+                    p: "+61 89164"
+                }
+
+                ListElement {
+                    n: "Cocos (Keeling) Islands"
+                    i: "CC"
+                    p: "+61 89162"
+                }
+
+                ListElement {
+                    n: "Colombia"
+                    i: "CO"
+                    p: "+57"
+                }
+
+                ListElement {
+                    n: "Comoros"
+                    i: "KM"
+                    p: "+269"
+                }
+
+                ListElement {
+                    n: "Congo (Democratic Republic of the)"
+                    i: "CD"
+                    p: "+243"
+                }
+
+                ListElement {
+                    n: "Congo"
+                    i: "CG"
+                    p: "+242"
+                }
+
+                ListElement {
+                    n: "Cook Islands"
+                    i: "CK"
+                    p: "+682"
+                }
+
+                ListElement {
+                    n: "Costa Rica"
+                    i: "CR"
+                    p: "+506"
+                }
+
+                ListElement {
+                    n: "Cรดte d'Ivoire"
+                    i: "CI"
+                    p: "+225"
+                }
+
+                ListElement {
+                    n: "Croatia"
+                    i: "HR"
+                    p: "+385"
+                }
+
+                ListElement {
+                    n: "Cuba"
+                    i: "CU"
+                    p: "+53"
+                }
+
+                ListElement {
+                    n: "Curaรงao"
+                    i: "CW"
+                    p: "+599 9"
+                }
+
+                ListElement {
+                    n: "Cyprus"
+                    i: "CY"
+                    p: "+357"
+                }
+
+                ListElement {
+                    n: "Czech Republic"
+                    i: "CZ"
+                    p: "+420"
+                }
+
+                ListElement {
+                    n: "Denmark"
+                    i: "DK"
+                    p: "+45"
+                }
+                //ListElement{n:"Diego Garcia";i:"";p:"+246"} // NO ISO, OCC. BY GB
+
+                ListElement {
+                    n: "Djibouti"
+                    i: "DJ"
+                    p: "+253"
+                }
+
+                ListElement {
+                    n: "Dominica"
+                    i: "DM"
+                    p: "+1 767"
+                }
+
+                ListElement {
+                    n: "Dominican Republic"
+                    i: "DO"
+                    p: "+1 809"
+                }
+
+                ListElement {
+                    n: "Dominican Republic"
+                    i: "DO"
+                    p: "+1 829"
+                }
+
+                ListElement {
+                    n: "Dominican Republic"
+                    i: "DO"
+                    p: "+1 849"
+                }
+
+                ListElement {
+                    n: "Easter Island"
+                    i: "CL"
+                    p: "+56"
+                }
+
+                ListElement {
+                    n: "Ecuador"
+                    i: "EC"
+                    p: "+593"
+                }
+
+                ListElement {
+                    n: "Egypt"
+                    i: "EG"
+                    p: "+20"
+                }
+
+                ListElement {
+                    n: "El Salvador"
+                    i: "SV"
+                    p: "+503"
+                }
+
+                ListElement {
+                    n: "Equatorial Guinea"
+                    i: "GQ"
+                    p: "+240"
+                }
+
+                ListElement {
+                    n: "Eritrea"
+                    i: "ER"
+                    p: "+291"
+                }
+
+                ListElement {
+                    n: "Estonia"
+                    i: "EE"
+                    p: "+372"
+                }
+
+                ListElement {
+                    n: "eSwatini"
+                    i: "SZ"
+                    p: "+268"
+                }
+
+                ListElement {
+                    n: "Ethiopia"
+                    i: "ET"
+                    p: "+251"
+                }
+
+                ListElement {
+                    n: "Falkland Islands (Malvinas)"
+                    i: "FK"
+                    p: "+500"
+                }
+
+                ListElement {
+                    n: "Faroe Islands"
+                    i: "FO"
+                    p: "+298"
+                }
+
+                ListElement {
+                    n: "Fiji"
+                    i: "FJ"
+                    p: "+679"
+                }
+
+                ListElement {
+                    n: "Finland"
+                    i: "FI"
+                    p: "+358"
+                }
+
+                ListElement {
+                    n: "France"
+                    i: "FR"
+                    p: "+33"
+                }
+                //ListElement{n:"French Antilles";i:"";p:"+596"} // NO ISO
+
+                ListElement {
+                    n: "French Guiana"
+                    i: "GF"
+                    p: "+594"
+                }
+
+                ListElement {
+                    n: "French Polynesia"
+                    i: "PF"
+                    p: "+689"
+                }
+
+                ListElement {
+                    n: "Gabon"
+                    i: "GA"
+                    p: "+241"
+                }
+
+                ListElement {
+                    n: "Gambia"
+                    i: "GM"
+                    p: "+220"
+                }
+
+                ListElement {
+                    n: "Georgia"
+                    i: "GE"
+                    p: "+995"
+                }
+
+                ListElement {
+                    n: "Germany"
+                    i: "DE"
+                    p: "+49"
+                }
+
+                ListElement {
+                    n: "Ghana"
+                    i: "GH"
+                    p: "+233"
+                }
+
+                ListElement {
+                    n: "Gibraltar"
+                    i: "GI"
+                    p: "+350"
+                }
+
+                ListElement {
+                    n: "Greece"
+                    i: "GR"
+                    p: "+30"
+                }
+
+                ListElement {
+                    n: "Greenland"
+                    i: "GL"
+                    p: "+299"
+                }
+
+                ListElement {
+                    n: "Grenada"
+                    i: "GD"
+                    p: "+1 473"
+                }
+
+                ListElement {
+                    n: "Guadeloupe"
+                    i: "GP"
+                    p: "+590"
+                }
+
+                ListElement {
+                    n: "Guam"
+                    i: "GU"
+                    p: "+1 671"
+                }
+
+                ListElement {
+                    n: "Guatemala"
+                    i: "GT"
+                    p: "+502"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 1481"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 7781"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 7839"
+                }
+
+                ListElement {
+                    n: "Guernsey"
+                    i: "GG"
+                    p: "+44 7911"
+                }
+
+                ListElement {
+                    n: "Guinea-Bissau"
+                    i: "GW"
+                    p: "+245"
+                }
+
+                ListElement {
+                    n: "Guinea"
+                    i: "GN"
+                    p: "+224"
+                }
+
+                ListElement {
+                    n: "Guyana"
+                    i: "GY"
+                    p: "+592"
+                }
+
+                ListElement {
+                    n: "Haiti"
+                    i: "HT"
+                    p: "+509"
+                }
+
+                ListElement {
+                    n: "Honduras"
+                    i: "HN"
+                    p: "+504"
+                }
+
+                ListElement {
+                    n: "Hong Kong"
+                    i: "HK"
+                    p: "+852"
+                }
+
+                ListElement {
+                    n: "Hungary"
+                    i: "HU"
+                    p: "+36"
+                }
+
+                ListElement {
+                    n: "Iceland"
+                    i: "IS"
+                    p: "+354"
+                }
+
+                ListElement {
+                    n: "India"
+                    i: "IN"
+                    p: "+91"
+                }
+
+                ListElement {
+                    n: "Indonesia"
+                    i: "ID"
+                    p: "+62"
+                }
+
+                ListElement {
+                    n: "Iran"
+                    i: "IR"
+                    p: "+98"
+                }
+
+                ListElement {
+                    n: "Iraq"
+                    i: "IQ"
+                    p: "+964"
+                }
+
+                ListElement {
+                    n: "Ireland"
+                    i: "IE"
+                    p: "+353"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 1624"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 7524"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 7624"
+                }
+
+                ListElement {
+                    n: "Isle of Man"
+                    i: "IM"
+                    p: "+44 7924"
+                }
+
+                ListElement {
+                    n: "Israel"
+                    i: "IL"
+                    p: "+972"
+                }
+
+                ListElement {
+                    n: "Italy"
+                    i: "IT"
+                    p: "+39"
+                }
+
+                ListElement {
+                    n: "Jamaica"
+                    i: "JM"
+                    p: "+1 876"
+                }
+
+                ListElement {
+                    n: "Jan Mayen"
+                    i: "SJ"
+                    p: "+47 79"
+                }
+
+                ListElement {
+                    n: "Japan"
+                    i: "JP"
+                    p: "+81"
+                }
+
+                ListElement {
+                    n: "Jersey"
+                    i: "JE"
+                    p: "+44 1534"
+                }
+
+                ListElement {
+                    n: "Jordan"
+                    i: "JO"
+                    p: "+962"
+                }
+
+                ListElement {
+                    n: "Kazakhstan"
+                    i: "KZ"
+                    p: "+7 6"
+                }
+
+                ListElement {
+                    n: "Kazakhstan"
+                    i: "KZ"
+                    p: "+7 7"
+                }
+
+                ListElement {
+                    n: "Kenya"
+                    i: "KE"
+                    p: "+254"
+                }
+
+                ListElement {
+                    n: "Kiribati"
+                    i: "KI"
+                    p: "+686"
+                }
+
+                ListElement {
+                    n: "Korea (North)"
+                    i: "KP"
+                    p: "+850"
+                }
+
+                ListElement {
+                    n: "Korea (South)"
+                    i: "KR"
+                    p: "+82"
+                }
+                // TEMP. CODE
+
+                ListElement {
+                    n: "Kosovo"
+                    i: "XK"
+                    p: "+383"
+                }
+
+                ListElement {
+                    n: "Kuwait"
+                    i: "KW"
+                    p: "+965"
+                }
+
+                ListElement {
+                    n: "Kyrgyzstan"
+                    i: "KG"
+                    p: "+996"
+                }
+
+                ListElement {
+                    n: "Laos"
+                    i: "LA"
+                    p: "+856"
+                }
+
+                ListElement {
+                    n: "Latvia"
+                    i: "LV"
+                    p: "+371"
+                }
+
+                ListElement {
+                    n: "Lebanon"
+                    i: "LB"
+                    p: "+961"
+                }
+
+                ListElement {
+                    n: "Lesotho"
+                    i: "LS"
+                    p: "+266"
+                }
+
+                ListElement {
+                    n: "Liberia"
+                    i: "LR"
+                    p: "+231"
+                }
+
+                ListElement {
+                    n: "Libya"
+                    i: "LY"
+                    p: "+218"
+                }
+
+                ListElement {
+                    n: "Liechtenstein"
+                    i: "LI"
+                    p: "+423"
+                }
+
+                ListElement {
+                    n: "Lithuania"
+                    i: "LT"
+                    p: "+370"
+                }
+
+                ListElement {
+                    n: "Luxembourg"
+                    i: "LU"
+                    p: "+352"
+                }
+
+                ListElement {
+                    n: "Macau (Macao)"
+                    i: "MO"
+                    p: "+853"
+                }
+
+                ListElement {
+                    n: "Madagascar"
+                    i: "MG"
+                    p: "+261"
+                }
+
+                ListElement {
+                    n: "Malawi"
+                    i: "MW"
+                    p: "+265"
+                }
+
+                ListElement {
+                    n: "Malaysia"
+                    i: "MY"
+                    p: "+60"
+                }
+
+                ListElement {
+                    n: "Maldives"
+                    i: "MV"
+                    p: "+960"
+                }
+
+                ListElement {
+                    n: "Mali"
+                    i: "ML"
+                    p: "+223"
+                }
+
+                ListElement {
+                    n: "Malta"
+                    i: "MT"
+                    p: "+356"
+                }
+
+                ListElement {
+                    n: "Marshall Islands"
+                    i: "MH"
+                    p: "+692"
+                }
+
+                ListElement {
+                    n: "Martinique"
+                    i: "MQ"
+                    p: "+596"
+                }
+
+                ListElement {
+                    n: "Mauritania"
+                    i: "MR"
+                    p: "+222"
+                }
+
+                ListElement {
+                    n: "Mauritius"
+                    i: "MU"
+                    p: "+230"
+                }
+
+                ListElement {
+                    n: "Mayotte"
+                    i: "YT"
+                    p: "+262 269"
+                }
+
+                ListElement {
+                    n: "Mayotte"
+                    i: "YT"
+                    p: "+262 639"
+                }
+
+                ListElement {
+                    n: "Mexico"
+                    i: "MX"
+                    p: "+52"
+                }
+
+                ListElement {
+                    n: "Micronesia (Federated States of)"
+                    i: "FM"
+                    p: "+691"
+                }
+
+                ListElement {
+                    n: "Midway Island (USA)"
+                    i: "US"
+                    p: "+1 808"
+                }
+
+                ListElement {
+                    n: "Moldova"
+                    i: "MD"
+                    p: "+373"
+                }
+
+                ListElement {
+                    n: "Monaco"
+                    i: "MC"
+                    p: "+377"
+                }
+
+                ListElement {
+                    n: "Mongolia"
+                    i: "MN"
+                    p: "+976"
+                }
+
+                ListElement {
+                    n: "Montenegro"
+                    i: "ME"
+                    p: "+382"
+                }
+
+                ListElement {
+                    n: "Montserrat"
+                    i: "MS"
+                    p: "+1 664"
+                }
+
+                ListElement {
+                    n: "Morocco"
+                    i: "MA"
+                    p: "+212"
+                }
+
+                ListElement {
+                    n: "Mozambique"
+                    i: "MZ"
+                    p: "+258"
+                }
+
+                ListElement {
+                    n: "Myanmar"
+                    i: "MM"
+                    p: "+95"
+                }
+                // NO OWN ISO, DISPUTED
+
+                ListElement {
+                    n: "Nagorno-Karabakh"
+                    i: "AZ"
+                    p: "+374 47"
+                }
+                // NO OWN ISO, DISPUTED
+
+                ListElement {
+                    n: "Nagorno-Karabakh"
+                    i: "AZ"
+                    p: "+374 97"
+                }
+
+                ListElement {
+                    n: "Namibia"
+                    i: "NA"
+                    p: "+264"
+                }
+
+                ListElement {
+                    n: "Nauru"
+                    i: "NR"
+                    p: "+674"
+                }
+
+                ListElement {
+                    n: "Nepal"
+                    i: "NP"
+                    p: "+977"
+                }
+
+                ListElement {
+                    n: "Netherlands"
+                    i: "NL"
+                    p: "+31"
+                }
+
+                ListElement {
+                    n: "Nevis"
+                    i: "KN"
+                    p: "+1 869"
+                }
+
+                ListElement {
+                    n: "New Caledonia"
+                    i: "NC"
+                    p: "+687"
+                }
+
+                ListElement {
+                    n: "New Zealand"
+                    i: "NZ"
+                    p: "+64"
+                }
+
+                ListElement {
+                    n: "Nicaragua"
+                    i: "NI"
+                    p: "+505"
+                }
+
+                ListElement {
+                    n: "Nigeria"
+                    i: "NG"
+                    p: "+234"
+                }
+
+                ListElement {
+                    n: "Niger"
+                    i: "NE"
+                    p: "+227"
+                }
+
+                ListElement {
+                    n: "Niue"
+                    i: "NU"
+                    p: "+683"
+                }
+
+                ListElement {
+                    n: "Norfolk Island"
+                    i: "NF"
+                    p: "+672 3"
+                }
+                // OCC. BY TR
+
+                ListElement {
+                    n: "Northern Cyprus"
+                    i: "CY"
+                    p: "+90 392"
+                }
+
+                ListElement {
+                    n: "Northern Ireland"
+                    i: "GB"
+                    p: "+44 28"
+                }
+
+                ListElement {
+                    n: "Northern Mariana Islands"
+                    i: "MP"
+                    p: "+1 670"
+                }
+
+                ListElement {
+                    n: "North Macedonia"
+                    i: "MK"
+                    p: "+389"
+                }
+
+                ListElement {
+                    n: "Norway"
+                    i: "NO"
+                    p: "+47"
+                }
+
+                ListElement {
+                    n: "Oman"
+                    i: "OM"
+                    p: "+968"
+                }
+
+                ListElement {
+                    n: "Pakistan"
+                    i: "PK"
+                    p: "+92"
+                }
+
+                ListElement {
+                    n: "Palau"
+                    i: "PW"
+                    p: "+680"
+                }
+
+                ListElement {
+                    n: "Palestine (State of)"
+                    i: "PS"
+                    p: "+970"
+                }
+
+                ListElement {
+                    n: "Panama"
+                    i: "PA"
+                    p: "+507"
+                }
+
+                ListElement {
+                    n: "Papua New Guinea"
+                    i: "PG"
+                    p: "+675"
+                }
+
+                ListElement {
+                    n: "Paraguay"
+                    i: "PY"
+                    p: "+595"
+                }
+
+                ListElement {
+                    n: "Peru"
+                    i: "PE"
+                    p: "+51"
+                }
+
+                ListElement {
+                    n: "Philippines"
+                    i: "PH"
+                    p: "+63"
+                }
+
+                ListElement {
+                    n: "Pitcairn Islands"
+                    i: "PN"
+                    p: "+64"
+                }
+
+                ListElement {
+                    n: "Poland"
+                    i: "PL"
+                    p: "+48"
+                }
+
+                ListElement {
+                    n: "Portugal"
+                    i: "PT"
+                    p: "+351"
+                }
+
+                ListElement {
+                    n: "Puerto Rico"
+                    i: "PR"
+                    p: "+1 787"
+                }
+
+                ListElement {
+                    n: "Puerto Rico"
+                    i: "PR"
+                    p: "+1 939"
+                }
+
+                ListElement {
+                    n: "Qatar"
+                    i: "QA"
+                    p: "+974"
+                }
+
+                ListElement {
+                    n: "Rรฉunion"
+                    i: "RE"
+                    p: "+262"
+                }
+
+                ListElement {
+                    n: "Romania"
+                    i: "RO"
+                    p: "+40"
+                }
+
+                ListElement {
+                    n: "Russia"
+                    i: "RU"
+                    p: "+7"
+                }
+
+                ListElement {
+                    n: "Rwanda"
+                    i: "RW"
+                    p: "+250"
+                }
+
+                ListElement {
+                    n: "Saba"
+                    i: "BQ"
+                    p: "+599 4"
+                }
+
+                ListElement {
+                    n: "Saint Barthรฉlemy"
+                    i: "BL"
+                    p: "+590"
+                }
+
+                ListElement {
+                    n: "Saint Helena"
+                    i: "SH"
+                    p: "+290"
+                }
+
+                ListElement {
+                    n: "Saint Kitts and Nevis"
+                    i: "KN"
+                    p: "+1 869"
+                }
+
+                ListElement {
+                    n: "Saint Lucia"
+                    i: "LC"
+                    p: "+1 758"
+                }
+
+                ListElement {
+                    n: "Saint Martin (France)"
+                    i: "MF"
+                    p: "+590"
+                }
+
+                ListElement {
+                    n: "Saint Pierre and Miquelon"
+                    i: "PM"
+                    p: "+508"
+                }
+
+                ListElement {
+                    n: "Saint Vincent and the Grenadines"
+                    i: "VC"
+                    p: "+1 784"
+                }
+
+                ListElement {
+                    n: "Samoa"
+                    i: "WS"
+                    p: "+685"
+                }
+
+                ListElement {
+                    n: "San Marino"
+                    i: "SM"
+                    p: "+378"
+                }
+
+                ListElement {
+                    n: "Sรฃo Tomรฉ and Prรญncipe"
+                    i: "ST"
+                    p: "+239"
+                }
+
+                ListElement {
+                    n: "Saudi Arabia"
+                    i: "SA"
+                    p: "+966"
+                }
+
+                ListElement {
+                    n: "Senegal"
+                    i: "SN"
+                    p: "+221"
+                }
+
+                ListElement {
+                    n: "Serbia"
+                    i: "RS"
+                    p: "+381"
+                }
+
+                ListElement {
+                    n: "Seychelles"
+                    i: "SC"
+                    p: "+248"
+                }
+
+                ListElement {
+                    n: "Sierra Leone"
+                    i: "SL"
+                    p: "+232"
+                }
+
+                ListElement {
+                    n: "Singapore"
+                    i: "SG"
+                    p: "+65"
+                }
+
+                ListElement {
+                    n: "Sint Eustatius"
+                    i: "BQ"
+                    p: "+599 3"
+                }
+
+                ListElement {
+                    n: "Sint Maarten (Netherlands)"
+                    i: "SX"
+                    p: "+1 721"
+                }
+
+                ListElement {
+                    n: "Slovakia"
+                    i: "SK"
+                    p: "+421"
+                }
+
+                ListElement {
+                    n: "Slovenia"
+                    i: "SI"
+                    p: "+386"
+                }
+
+                ListElement {
+                    n: "Solomon Islands"
+                    i: "SB"
+                    p: "+677"
+                }
+
+                ListElement {
+                    n: "Somalia"
+                    i: "SO"
+                    p: "+252"
+                }
+
+                ListElement {
+                    n: "South Africa"
+                    i: "ZA"
+                    p: "+27"
+                }
+
+                ListElement {
+                    n: "South Georgia and the South Sandwich Islands"
+                    i: "GS"
+                    p: "+500"
+                }
+                // NO OWN ISO, DISPUTED
+
+                ListElement {
+                    n: "South Ossetia"
+                    i: "GE"
+                    p: "+995 34"
+                }
+
+                ListElement {
+                    n: "South Sudan"
+                    i: "SS"
+                    p: "+211"
+                }
+
+                ListElement {
+                    n: "Spain"
+                    i: "ES"
+                    p: "+34"
+                }
+
+                ListElement {
+                    n: "Sri Lanka"
+                    i: "LK"
+                    p: "+94"
+                }
+
+                ListElement {
+                    n: "Sudan"
+                    i: "SD"
+                    p: "+249"
+                }
+
+                ListElement {
+                    n: "Suriname"
+                    i: "SR"
+                    p: "+597"
+                }
+
+                ListElement {
+                    n: "Svalbard"
+                    i: "SJ"
+                    p: "+47 79"
+                }
+
+                ListElement {
+                    n: "Sweden"
+                    i: "SE"
+                    p: "+46"
+                }
+
+                ListElement {
+                    n: "Switzerland"
+                    i: "CH"
+                    p: "+41"
+                }
+
+                ListElement {
+                    n: "Syria"
+                    i: "SY"
+                    p: "+963"
+                }
+
+                ListElement {
+                    n: "Taiwan"
+                    i: "SJ"
+                    p: "+886"
+                }
+
+                ListElement {
+                    n: "Tajikistan"
+                    i: "TJ"
+                    p: "+992"
+                }
+
+                ListElement {
+                    n: "Tanzania"
+                    i: "TZ"
+                    p: "+255"
+                }
+
+                ListElement {
+                    n: "Thailand"
+                    i: "TH"
+                    p: "+66"
+                }
+
+                ListElement {
+                    n: "Timor-Leste"
+                    i: "TL"
+                    p: "+670"
+                }
+
+                ListElement {
+                    n: "Togo"
+                    i: "TG"
+                    p: "+228"
+                }
+
+                ListElement {
+                    n: "Tokelau"
+                    i: "TK"
+                    p: "+690"
+                }
+
+                ListElement {
+                    n: "Tonga"
+                    i: "TO"
+                    p: "+676"
+                }
+
+                ListElement {
+                    n: "Transnistria"
+                    i: "MD"
+                    p: "+373 2"
+                }
+
+                ListElement {
+                    n: "Transnistria"
+                    i: "MD"
+                    p: "+373 5"
+                }
+
+                ListElement {
+                    n: "Trinidad and Tobago"
+                    i: "TT"
+                    p: "+1 868"
+                }
+
+                ListElement {
+                    n: "Tristan da Cunha"
+                    i: "SH"
+                    p: "+290 8"
+                }
+
+                ListElement {
+                    n: "Tunisia"
+                    i: "TN"
+                    p: "+216"
+                }
+
+                ListElement {
+                    n: "Turkey"
+                    i: "TR"
+                    p: "+90"
+                }
+
+                ListElement {
+                    n: "Turkmenistan"
+                    i: "TM"
+                    p: "+993"
+                }
+
+                ListElement {
+                    n: "Turks and Caicos Islands"
+                    i: "TC"
+                    p: "+1 649"
+                }
+
+                ListElement {
+                    n: "Tuvalu"
+                    i: "TV"
+                    p: "+688"
+                }
+
+                ListElement {
+                    n: "Uganda"
+                    i: "UG"
+                    p: "+256"
+                }
+
+                ListElement {
+                    n: "Ukraine"
+                    i: "UA"
+                    p: "+380"
+                }
+
+                ListElement {
+                    n: "United Arab Emirates"
+                    i: "AE"
+                    p: "+971"
+                }
+
+                ListElement {
+                    n: "United Kingdom"
+                    i: "GB"
+                    p: "+44"
+                }
+
+                ListElement {
+                    n: "United States"
+                    i: "US"
+                    p: "+1"
+                }
+
+                ListElement {
+                    n: "Uruguay"
+                    i: "UY"
+                    p: "+598"
+                }
+
+                ListElement {
+                    n: "Uzbekistan"
+                    i: "UZ"
+                    p: "+998"
+                }
+
+                ListElement {
+                    n: "Vanuatu"
+                    i: "VU"
+                    p: "+678"
+                }
+
+                ListElement {
+                    n: "Vatican City State (Holy See)"
+                    i: "VA"
+                    p: "+379"
+                }
+
+                ListElement {
+                    n: "Vatican City State (Holy See)"
+                    i: "VA"
+                    p: "+39 06 698"
+                }
+
+                ListElement {
+                    n: "Venezuela"
+                    i: "VE"
+                    p: "+58"
+                }
+
+                ListElement {
+                    n: "Vietnam"
+                    i: "VN"
+                    p: "+84"
+                }
+
+                ListElement {
+                    n: "Virgin Islands (British)"
+                    i: "VG"
+                    p: "+1 284"
+                }
+
+                ListElement {
+                    n: "Virgin Islands (US)"
+                    i: "VI"
+                    p: "+1 340"
+                }
+
+                ListElement {
+                    n: "Wake Island (USA)"
+                    i: "US"
+                    p: "+1 808"
+                }
+
+                ListElement {
+                    n: "Wallis and Futuna"
+                    i: "WF"
+                    p: "+681"
+                }
+
+                ListElement {
+                    n: "Yemen"
+                    i: "YE"
+                    p: "+967"
+                }
+
+                ListElement {
+                    n: "Zambia"
+                    i: "ZM"
+                    p: "+260"
+                }
+                // NO OWN ISO, DISPUTED?
+
+                ListElement {
+                    n: "Zanzibar"
+                    i: "TZ"
+                    p: "+255 24"
+                }
+
+                ListElement {
+                    n: "Zimbabwe"
+                    i: "ZW"
+                    p: "+263"
+                }
+
+            }
+
+        }
+
+        MatrixTextField {
+            id: statusInput
+
+            Layout.fillWidth: true
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+        onAccepted: {
+            if (inputDialog.onAccepted)
+                inputDialog.onAccepted(numberPrefix.model.get(numberPrefix.currentIndex).i, statusInput.text);
+
+            inputDialog.close();
+        }
+        onRejected: {
+            inputDialog.close();
+        }
+    }
+
+}
diff --git a/resources/qml/RawMessageDialog.qml b/resources/qml/dialogs/RawMessageDialog.qml
similarity index 97%
rename from resources/qml/RawMessageDialog.qml
rename to resources/qml/dialogs/RawMessageDialog.qml
index e2a476cd..c171de7e 100644
--- a/resources/qml/RawMessageDialog.qml
+++ b/resources/qml/dialogs/RawMessageDialog.qml
@@ -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 {
diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/dialogs/ReadReceipts.qml
similarity index 97%
rename from resources/qml/ReadReceipts.qml
rename to resources/qml/dialogs/ReadReceipts.qml
index 9adbfd5c..e825dd81 100644
--- a/resources/qml/ReadReceipts.qml
+++ b/resources/qml/dialogs/ReadReceipts.qml
@@ -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 {
diff --git a/resources/qml/dialogs/RoomDirectory.qml b/resources/qml/dialogs/RoomDirectory.qml
new file mode 100644
index 00000000..bb55b27c
--- /dev/null
+++ b/resources/qml/dialogs/RoomDirectory.qml
@@ -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)
+        }
+
+    }
+
+}
diff --git a/resources/qml/RoomMembers.qml b/resources/qml/dialogs/RoomMembers.qml
similarity index 97%
rename from resources/qml/RoomMembers.qml
rename to resources/qml/dialogs/RoomMembers.qml
index 8e44855c..b2806292 100644
--- a/resources/qml/RoomMembers.qml
+++ b/resources/qml/dialogs/RoomMembers.qml
@@ -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/")
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml
similarity index 93%
rename from resources/qml/RoomSettings.qml
rename to resources/qml/dialogs/RoomSettings.qml
index 491a336f..0e7749ce 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/dialogs/RoomSettings.qml
@@ -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);
diff --git a/resources/qml/dialogs/UserProfile.qml b/resources/qml/dialogs/UserProfile.qml
new file mode 100644
index 00000000..da573ec1
--- /dev/null
+++ b/resources/qml/dialogs/UserProfile.qml
@@ -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
+            }
+
+        }
+
+    }
+
+}
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index 354e340c..e83f8a5e 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -72,7 +72,8 @@ Menu {
                 onVisibleChanged: {
                     if (visible)
                         forceActiveFocus();
-
+                    else
+                        clear();
                 }
 
                 Timer {
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index d44c5edf..be698356 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -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"
         }
 
diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml
index 253fa25c..1bd5eb26 100644
--- a/resources/qml/voip/CallInvite.qml
+++ b/resources/qml/voip/CallInvite.qml
@@ -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 {
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index f6c1ecde..10f8367a 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -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"
         }
 
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 97932cc9..c733012c 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -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)
             }
 
diff --git a/resources/res.qrc b/resources/res.qrc
index e1761cc0..ccb5a637 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -73,6 +73,7 @@
         icons/ui/screen-share.png
         icons/ui/toggle-camera-view.png
         icons/ui/video-call.png
+        icons/ui/refresh.png
         icons/emoji-categories/people.png
         icons/emoji-categories/people@2x.png
         icons/emoji-categories/nature.png
@@ -138,31 +139,47 @@
         qml/TopBar.qml
         qml/QuickSwitcher.qml
         qml/ForwardCompleter.qml
+        qml/SelfVerificationCheck.qml
         qml/TypingIndicator.qml
-        qml/RoomSettings.qml
-        qml/emoji/EmojiPicker.qml
-        qml/emoji/StickerPicker.qml
-        qml/UserProfile.qml
-        qml/delegates/MessageDelegate.qml
+        qml/NotificationWarning.qml
+        qml/components/AdaptiveLayout.qml
+        qml/components/AdaptiveLayoutElement.qml
+        qml/components/AvatarListTile.qml
+        qml/components/FlatButton.qml
+        qml/components/MainWindowDialog.qml
         qml/delegates/Encrypted.qml
         qml/delegates/FileMessage.qml
         qml/delegates/ImageMessage.qml
+        qml/delegates/MessageDelegate.qml
         qml/delegates/NoticeMessage.qml
         qml/delegates/Pill.qml
         qml/delegates/Placeholder.qml
         qml/delegates/PlayableMediaMessage.qml
         qml/delegates/Reply.qml
         qml/delegates/TextMessage.qml
-        qml/device-verification/Waiting.qml
         qml/device-verification/DeviceVerification.qml
         qml/device-verification/DigitVerification.qml
         qml/device-verification/EmojiVerification.qml
-        qml/device-verification/NewVerificationRequest.qml
         qml/device-verification/Failed.qml
+        qml/device-verification/NewVerificationRequest.qml
         qml/device-verification/Success.qml
-        qml/dialogs/InputDialog.qml
-        qml/dialogs/ImagePackSettingsDialog.qml
+        qml/device-verification/Waiting.qml
         qml/dialogs/ImagePackEditorDialog.qml
+        qml/dialogs/ImagePackSettingsDialog.qml
+        qml/dialogs/PhoneNumberInputDialog.qml
+        qml/dialogs/InputDialog.qml
+        qml/dialogs/InviteDialog.qml
+        qml/dialogs/JoinRoomDialog.qml
+        qml/dialogs/LeaveRoomDialog.qml
+        qml/dialogs/LogoutDialog.qml
+        qml/dialogs/RawMessageDialog.qml
+        qml/dialogs/ReadReceipts.qml
+        qml/dialogs/RoomDirectory.qml
+        qml/dialogs/RoomMembers.qml
+        qml/dialogs/RoomSettings.qml
+        qml/dialogs/UserProfile.qml
+        qml/emoji/EmojiPicker.qml
+        qml/emoji/StickerPicker.qml
         qml/ui/Ripple.qml
         qml/ui/Spinner.qml
         qml/ui/animations/BlinkAnimation.qml
@@ -174,14 +191,6 @@
         qml/voip/PlaceCall.qml
         qml/voip/ScreenShare.qml
         qml/voip/VideoCall.qml
-        qml/components/AdaptiveLayout.qml
-        qml/components/AdaptiveLayoutElement.qml
-        qml/components/AvatarListTile.qml
-        qml/components/FlatButton.qml
-        qml/RoomMembers.qml
-        qml/InviteDialog.qml
-        qml/ReadReceipts.qml
-        qml/RawMessageDialog.qml
     
     
         media/ring.ogg
diff --git a/scripts/emoji_codegen.py b/scripts/emoji_codegen.py
index df581036..88f711f3 100755
--- a/scripts/emoji_codegen.py
+++ b/scripts/emoji_codegen.py
@@ -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()
 
diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp
index b9962cef..177bf903 100644
--- a/src/AvatarProvider.cpp
+++ b/src/AvatarProvider.cpp
@@ -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(receiver)](
-                                     QString, QSize, QImage img, QString) {
-                                           if (!recv)
-                                                   return;
+    MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")),
+                               QSize(size, size),
+                               [callback, cacheKey, recv = QPointer(receiver)](
+                                 QString, QSize, QImage img, QString) {
+                                   if (!recv)
+                                       return;
 
-                                           auto proxy = std::make_shared();
-                                           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();
+                                   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);
 }
 }
diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h
index 173a2fba..efd0f330 100644
--- a/src/AvatarProvider.h
+++ b/src/AvatarProvider.h
@@ -12,10 +12,10 @@ using AvatarCallback = std::function;
 
 class AvatarProxy : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 signals:
-        void avatarDownloaded(QPixmap pm);
+    void avatarDownloaded(QPixmap pm);
 };
 
 namespace AvatarProvider {
diff --git a/src/BlurhashProvider.cpp b/src/BlurhashProvider.cpp
index aef618a2..e905474a 100644
--- a/src/BlurhashProvider.cpp
+++ b/src/BlurhashProvider.cpp
@@ -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();
 }
diff --git a/src/BlurhashProvider.h b/src/BlurhashProvider.h
index ee89302c..1c8351f2 100644
--- a/src/BlurhashProvider.h
+++ b/src/BlurhashProvider.h
@@ -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;
 };
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 8b8b2985..58eb2630 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -13,6 +13,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #if __has_include()
@@ -29,19 +30,19 @@
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MatrixClient.h"
-#include "Olm.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "encryption/Olm.h"
 
 //! Should be changed when a breaking change occurs in the cache format.
 //! This will reset client's data.
-static const std::string CURRENT_CACHE_FORMAT_VERSION("2020.10.20");
-static const std::string SECRET("secret");
+static const std::string CURRENT_CACHE_FORMAT_VERSION("2021.08.31");
 
 //! Keys used for the DB
 static const std::string_view NEXT_BATCH_KEY("next_batch");
 static const std::string_view OLM_ACCOUNT_KEY("olm_account");
 static const std::string_view CACHE_FORMAT_VERSION_KEY("cache_format_version");
+static const std::string_view CURRENT_ONLINE_BACKUP_VERSION("current_online_backup_version");
 
 constexpr size_t MAX_RESTORED_MESSAGES = 30'000;
 
@@ -96,55 +97,55 @@ std::unique_ptr instance_ = nullptr;
 
 struct RO_txn
 {
-        ~RO_txn() { txn.reset(); }
-        operator MDB_txn *() const noexcept { return txn.handle(); }
-        operator lmdb::txn &() noexcept { return txn; }
+    ~RO_txn() { txn.reset(); }
+    operator MDB_txn *() const noexcept { return txn.handle(); }
+    operator lmdb::txn &() noexcept { return txn; }
 
-        lmdb::txn &txn;
+    lmdb::txn &txn;
 };
 
 RO_txn
 ro_txn(lmdb::env &env)
 {
-        thread_local lmdb::txn txn     = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
-        thread_local int reuse_counter = 0;
+    thread_local lmdb::txn txn     = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+    thread_local int reuse_counter = 0;
 
-        if (reuse_counter >= 100 || txn.env() != env.handle()) {
-                txn.abort();
-                txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
-                reuse_counter = 0;
-        } else if (reuse_counter > 0) {
-                try {
-                        txn.renew();
-                } catch (...) {
-                        txn.abort();
-                        txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
-                        reuse_counter = 0;
-                }
+    if (reuse_counter >= 100 || txn.env() != env.handle()) {
+        txn.abort();
+        txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+        reuse_counter = 0;
+    } else if (reuse_counter > 0) {
+        try {
+            txn.renew();
+        } catch (...) {
+            txn.abort();
+            txn           = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+            reuse_counter = 0;
         }
-        reuse_counter++;
+    }
+    reuse_counter++;
 
-        return RO_txn{txn};
+    return RO_txn{txn};
 }
 
 template
 bool
 containsStateUpdates(const T &e)
 {
-        return std::visit([](const auto &ev) { return Cache::isStateEvent_; }, e);
+    return std::visit([](const auto &ev) { return Cache::isStateEvent_; }, e);
 }
 
 bool
 containsStateUpdates(const mtx::events::collections::StrippedEvents &e)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        return std::holds_alternative>(e) ||
-               std::holds_alternative>(e) ||
-               std::holds_alternative>(e) ||
-               std::holds_alternative>(e) ||
-               std::holds_alternative>(e);
+    return std::holds_alternative>(e) ||
+           std::holds_alternative>(e) ||
+           std::holds_alternative>(e) ||
+           std::holds_alternative>(e) ||
+           std::holds_alternative>(e);
 }
 
 bool
@@ -152,45 +153,45 @@ Cache::isHiddenEvent(lmdb::txn &txn,
                      mtx::events::collections::TimelineEvents e,
                      const std::string &room_id)
 {
-        using namespace mtx::events;
+    using namespace mtx::events;
 
-        // Always hide edits
-        if (mtx::accessors::relations(e).replaces())
-                return true;
+    // Always hide edits
+    if (mtx::accessors::relations(e).replaces())
+        return true;
 
-        if (auto encryptedEvent = std::get_if>(&e)) {
-                MegolmSessionIndex index;
-                index.room_id    = room_id;
-                index.session_id = encryptedEvent->content.session_id;
-                index.sender_key = encryptedEvent->content.sender_key;
+    if (auto encryptedEvent = std::get_if>(&e)) {
+        MegolmSessionIndex index;
+        index.room_id    = room_id;
+        index.session_id = encryptedEvent->content.session_id;
+        index.sender_key = encryptedEvent->content.sender_key;
 
-                auto result = olm::decryptEvent(index, *encryptedEvent, true);
-                if (!result.error)
-                        e = result.event.value();
-        }
+        auto result = olm::decryptEvent(index, *encryptedEvent, true);
+        if (!result.error)
+            e = result.event.value();
+    }
 
-        mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents;
-        hiddenEvents.hidden_event_types = {
-          EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
+    mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents;
+    hiddenEvents.hidden_event_types = {
+      EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
 
-        if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, ""))
-                hiddenEvents =
-                  std::move(std::get>(*temp)
-                              .content);
-        if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id))
-                hiddenEvents =
-                  std::move(std::get>(*temp)
-                              .content);
+    if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, ""))
+        hiddenEvents =
+          std::move(std::get>(*temp)
+                      .content);
+    if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id))
+        hiddenEvents =
+          std::move(std::get>(*temp)
+                      .content);
 
-        return std::visit(
-          [hiddenEvents](const auto &ev) {
-                  return std::any_of(hiddenEvents.hidden_event_types.begin(),
-                                     hiddenEvents.hidden_event_types.end(),
-                                     [ev](EventType type) { return type == ev.type; });
-          },
-          e);
+    return std::visit(
+      [hiddenEvents](const auto &ev) {
+          return std::any_of(hiddenEvents.hidden_event_types.begin(),
+                             hiddenEvents.hidden_event_types.end(),
+                             [ev](EventType type) { return type == ev.type; });
+      },
+      e);
 }
 
 Cache::Cache(const QString &userId, QObject *parent)
@@ -198,217 +199,221 @@ Cache::Cache(const QString &userId, QObject *parent)
   , env_{nullptr}
   , localUserId_{userId}
 {
-        setup();
-        connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
+    setup();
+    connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
+    connect(
+      this,
+      &Cache::verificationStatusChanged,
+      this,
+      [this](const std::string &u) {
+          if (u == localUserId_.toStdString()) {
+              auto status = verificationStatus(u);
+              emit selfVerificationStatusChanged();
+          }
+      },
+      Qt::QueuedConnection);
 }
 
 void
 Cache::setup()
 {
-        auto settings = UserSettings::instance();
+    auto settings = UserSettings::instance();
 
-        nhlog::db()->debug("setting up cache");
+    nhlog::db()->debug("setting up cache");
 
-        // Previous location of the cache directory
-        auto oldCache = QString("%1/%2%3")
-                          .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
-                          .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
-                          .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
+    // Previous location of the cache directory
+    auto oldCache = QString("%1/%2%3")
+                      .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
+                      .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
+                      .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
 
-        cacheDirectory_ = QString("%1/%2%3")
-                            .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))
-                            .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
-                            .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
+    cacheDirectory_ = QString("%1/%2%3")
+                        .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))
+                        .arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
+                        .arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
 
-        bool isInitial = !QFile::exists(cacheDirectory_);
+    bool isInitial = !QFile::exists(cacheDirectory_);
 
-        // NOTE: If both cache directories exist it's better to do nothing: it
-        // could mean a previous migration failed or was interrupted.
-        bool needsMigration = isInitial && QFile::exists(oldCache);
+    // NOTE: If both cache directories exist it's better to do nothing: it
+    // could mean a previous migration failed or was interrupted.
+    bool needsMigration = isInitial && QFile::exists(oldCache);
 
-        if (needsMigration) {
-                nhlog::db()->info("found old state directory, migrating");
-                if (!QDir().rename(oldCache, cacheDirectory_)) {
-                        throw std::runtime_error(("Unable to migrate the old state directory (" +
-                                                  oldCache + ") to the new location (" +
-                                                  cacheDirectory_ + ")")
-                                                   .toStdString()
-                                                   .c_str());
-                }
-                nhlog::db()->info("completed state migration");
+    if (needsMigration) {
+        nhlog::db()->info("found old state directory, migrating");
+        if (!QDir().rename(oldCache, cacheDirectory_)) {
+            throw std::runtime_error(("Unable to migrate the old state directory (" + oldCache +
+                                      ") to the new location (" + cacheDirectory_ + ")")
+                                       .toStdString()
+                                       .c_str());
+        }
+        nhlog::db()->info("completed state migration");
+    }
+
+    env_ = lmdb::env::create();
+    env_.set_mapsize(DB_SIZE);
+    env_.set_max_dbs(MAX_DBS);
+
+    if (isInitial) {
+        nhlog::db()->info("initializing LMDB");
+
+        if (!QDir().mkpath(cacheDirectory_)) {
+            throw std::runtime_error(
+              ("Unable to create state directory:" + cacheDirectory_).toStdString().c_str());
+        }
+    }
+
+    try {
+        // NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
+        // it can really mess up our database, so we shouldn't. For now, hopefully
+        // NOMETASYNC is fast enough.
+        env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC);
+    } catch (const lmdb::error &e) {
+        if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
+            throw std::runtime_error("LMDB initialization failed" + std::string(e.what()));
         }
 
-        env_ = lmdb::env::create();
-        env_.set_mapsize(DB_SIZE);
-        env_.set_max_dbs(MAX_DBS);
+        nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
 
-        if (isInitial) {
-                nhlog::db()->info("initializing LMDB");
+        QDir stateDir(cacheDirectory_);
 
-                if (!QDir().mkpath(cacheDirectory_)) {
-                        throw std::runtime_error(
-                          ("Unable to create state directory:" + cacheDirectory_)
-                            .toStdString()
-                            .c_str());
-                }
+        for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
+            if (!stateDir.remove(file))
+                throw std::runtime_error(("Unable to delete file " + file).toStdString().c_str());
         }
+        env_.open(cacheDirectory_.toStdString().c_str());
+    }
 
-        try {
-                // NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
-                // it can really mess up our database, so we shouldn't. For now, hopefully
-                // NOMETASYNC is fast enough.
-                env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC);
-        } catch (const lmdb::error &e) {
-                if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
-                        throw std::runtime_error("LMDB initialization failed" +
-                                                 std::string(e.what()));
-                }
+    auto txn          = lmdb::txn::begin(env_);
+    syncStateDb_      = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
+    roomsDb_          = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
+    spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT);
+    spacesParentsDb_  = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT);
+    invitesDb_        = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
+    readReceiptsDb_   = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
+    notificationsDb_  = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
 
-                nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
+    // Device management
+    devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
+    deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
 
-                QDir stateDir(cacheDirectory_);
+    // Session management
+    inboundMegolmSessionDb_  = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+    outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+    megolmSessionDataDb_     = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
 
-                for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
-                        if (!stateDir.remove(file))
-                                throw std::runtime_error(
-                                  ("Unable to delete file " + file).toStdString().c_str());
-                }
-                env_.open(cacheDirectory_.toStdString().c_str());
-        }
+    // What rooms are encrypted
+    encryptedRooms_                      = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+    [[maybe_unused]] auto verificationDb = getVerificationDb(txn);
+    [[maybe_unused]] auto userKeysDb     = getUserKeysDb(txn);
 
-        auto txn          = lmdb::txn::begin(env_);
-        syncStateDb_      = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
-        roomsDb_          = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
-        spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT);
-        spacesParentsDb_  = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT);
-        invitesDb_        = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
-        readReceiptsDb_   = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
-        notificationsDb_  = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
+    txn.commit();
 
-        // Device management
-        devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
-        deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
-
-        // Session management
-        inboundMegolmSessionDb_  = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
-        outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
-        megolmSessionDataDb_     = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
-
-        // What rooms are encrypted
-        encryptedRooms_                      = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
-        [[maybe_unused]] auto verificationDb = getVerificationDb(txn);
-        [[maybe_unused]] auto userKeysDb     = getUserKeysDb(txn);
-
-        txn.commit();
-
-        databaseReady_ = true;
+    databaseReady_ = true;
 }
 
 void
 Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
 {
-        nhlog::db()->info("mark room {} as encrypted", room_id);
+    nhlog::db()->info("mark room {} as encrypted", room_id);
 
-        encryptedRooms_.put(txn, room_id, "0");
+    encryptedRooms_.put(txn, room_id, "0");
 }
 
 bool
 Cache::isRoomEncrypted(const std::string &room_id)
 {
-        std::string_view unused;
+    std::string_view unused;
 
-        auto txn = ro_txn(env_);
-        auto res = encryptedRooms_.get(txn, room_id, unused);
+    auto txn = ro_txn(env_);
+    auto res = encryptedRooms_.get(txn, room_id, unused);
 
-        return res;
+    return res;
 }
 
 std::optional
 Cache::roomEncryptionSettings(const std::string &room_id)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        try {
-                auto txn      = ro_txn(env_);
-                auto statesdb = getStatesDb(txn, room_id);
-                std::string_view event;
-                bool res =
-                  statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event);
+    try {
+        auto txn      = ro_txn(env_);
+        auto statesdb = getStatesDb(txn, room_id);
+        std::string_view event;
+        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event);
 
-                if (res) {
-                        try {
-                                StateEvent msg = json::parse(event);
+        if (res) {
+            try {
+                StateEvent msg = json::parse(event);
 
-                                return msg.content;
-                        } catch (const json::exception &e) {
-                                nhlog::db()->warn("failed to parse m.room.encryption event: {}",
-                                                  e.what());
-                                return Encryption{};
-                        }
-                }
-        } catch (lmdb::error &) {
+                return msg.content;
+            } catch (const json::exception &e) {
+                nhlog::db()->warn("failed to parse m.room.encryption event: {}", e.what());
+                return Encryption{};
+            }
         }
+    } catch (lmdb::error &) {
+    }
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 mtx::crypto::ExportedSessionKeys
 Cache::exportSessionKeys()
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        ExportedSessionKeys keys;
+    ExportedSessionKeys keys;
 
-        auto txn    = ro_txn(env_);
-        auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
+    auto txn    = ro_txn(env_);
+    auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
 
-        std::string_view key, value;
-        while (cursor.get(key, value, MDB_NEXT)) {
-                ExportedSession exported;
-                MegolmSessionIndex index;
+    std::string_view key, value;
+    while (cursor.get(key, value, MDB_NEXT)) {
+        ExportedSession exported;
+        MegolmSessionIndex index;
 
-                auto saved_session = unpickle(std::string(value), SECRET);
+        auto saved_session = unpickle(std::string(value), pickle_secret_);
 
-                try {
-                        index = nlohmann::json::parse(key).get();
-                } catch (const nlohmann::json::exception &e) {
-                        nhlog::db()->critical("failed to export megolm session: {}", e.what());
-                        continue;
-                }
-
-                exported.room_id     = index.room_id;
-                exported.sender_key  = index.sender_key;
-                exported.session_id  = index.session_id;
-                exported.session_key = export_session(saved_session.get(), -1);
-
-                keys.sessions.push_back(exported);
+        try {
+            index = nlohmann::json::parse(key).get();
+        } catch (const nlohmann::json::exception &e) {
+            nhlog::db()->critical("failed to export megolm session: {}", e.what());
+            continue;
         }
 
-        cursor.close();
+        exported.room_id     = index.room_id;
+        exported.sender_key  = index.sender_key;
+        exported.session_id  = index.session_id;
+        exported.session_key = export_session(saved_session.get(), -1);
 
-        return keys;
+        keys.sessions.push_back(exported);
+    }
+
+    cursor.close();
+
+    return keys;
 }
 
 void
 Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
 {
-        for (const auto &s : keys.sessions) {
-                MegolmSessionIndex index;
-                index.room_id    = s.room_id;
-                index.session_id = s.session_id;
-                index.sender_key = s.sender_key;
+    for (const auto &s : keys.sessions) {
+        MegolmSessionIndex index;
+        index.room_id    = s.room_id;
+        index.session_id = s.session_id;
+        index.sender_key = s.sender_key;
 
-                GroupSessionData data{};
-                data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain;
-                if (s.sender_claimed_keys.count("ed25519"))
-                        data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519");
+        GroupSessionData data{};
+        data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain;
+        if (s.sender_claimed_keys.count("ed25519"))
+            data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519");
 
-                auto exported_session = mtx::crypto::import_session(s.session_key);
+        auto exported_session = mtx::crypto::import_session(s.session_key);
 
-                saveInboundMegolmSession(index, std::move(exported_session), data);
-                ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
-        }
+        saveInboundMegolmSession(index, std::move(exported_session), data);
+        ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
+    }
 }
 
 //
@@ -420,65 +425,64 @@ Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
                                 mtx::crypto::InboundGroupSessionPtr session,
                                 const GroupSessionData &data)
 {
-        using namespace mtx::crypto;
-        const auto key     = json(index).dump();
-        const auto pickled = pickle(session.get(), SECRET);
+    using namespace mtx::crypto;
+    const auto key     = json(index).dump();
+    const auto pickled = pickle(session.get(), pickle_secret_);
 
-        auto txn = lmdb::txn::begin(env_);
+    auto txn = lmdb::txn::begin(env_);
 
-        std::string_view value;
-        if (inboundMegolmSessionDb_.get(txn, key, value)) {
-                auto oldSession = unpickle(std::string(value), SECRET);
-                if (olm_inbound_group_session_first_known_index(session.get()) >
-                    olm_inbound_group_session_first_known_index(oldSession.get())) {
-                        nhlog::crypto()->warn(
-                          "Not storing inbound session with newer first known index");
-                        return;
-                }
+    std::string_view value;
+    if (inboundMegolmSessionDb_.get(txn, key, value)) {
+        auto oldSession = unpickle(std::string(value), pickle_secret_);
+        if (olm_inbound_group_session_first_known_index(session.get()) >
+            olm_inbound_group_session_first_known_index(oldSession.get())) {
+            nhlog::crypto()->warn("Not storing inbound session with newer first known index");
+            return;
         }
+    }
 
-        inboundMegolmSessionDb_.put(txn, key, pickled);
-        megolmSessionDataDb_.put(txn, key, json(data).dump());
-        txn.commit();
+    inboundMegolmSessionDb_.put(txn, key, pickled);
+    megolmSessionDataDb_.put(txn, key, json(data).dump());
+    txn.commit();
 }
 
 mtx::crypto::InboundGroupSessionPtr
 Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        try {
-                auto txn        = ro_txn(env_);
-                std::string key = json(index).dump();
-                std::string_view value;
+    try {
+        auto txn        = ro_txn(env_);
+        std::string key = json(index).dump();
+        std::string_view value;
 
-                if (inboundMegolmSessionDb_.get(txn, key, value)) {
-                        auto session = unpickle(std::string(value), SECRET);
-                        return session;
-                }
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
+        if (inboundMegolmSessionDb_.get(txn, key, value)) {
+            auto session = unpickle(std::string(value), pickle_secret_);
+            return session;
         }
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
+    }
 
-        return nullptr;
+    return nullptr;
 }
 
 bool
 Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        try {
-                auto txn        = ro_txn(env_);
-                std::string key = json(index).dump();
-                std::string_view value;
+    try {
+        auto txn        = ro_txn(env_);
+        std::string key = json(index).dump();
+        std::string_view value;
 
-                return inboundMegolmSessionDb_.get(txn, key, value);
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
-        }
+        return inboundMegolmSessionDb_.get(txn, key, value);
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
+    }
 
-        return false;
+    return false;
 }
 
 void
@@ -486,42 +490,42 @@ Cache::updateOutboundMegolmSession(const std::string &room_id,
                                    const GroupSessionData &data_,
                                    mtx::crypto::OutboundGroupSessionPtr &ptr)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        if (!outboundMegolmSessionExists(room_id))
-                return;
+    if (!outboundMegolmSessionExists(room_id))
+        return;
 
-        GroupSessionData data = data_;
-        data.message_index    = olm_outbound_group_session_message_index(ptr.get());
-        MegolmSessionIndex index;
-        index.room_id    = room_id;
-        index.sender_key = olm::client()->identity_keys().ed25519;
-        index.session_id = mtx::crypto::session_id(ptr.get());
+    GroupSessionData data = data_;
+    data.message_index    = olm_outbound_group_session_message_index(ptr.get());
+    MegolmSessionIndex index;
+    index.room_id    = room_id;
+    index.sender_key = olm::client()->identity_keys().ed25519;
+    index.session_id = mtx::crypto::session_id(ptr.get());
 
-        // Save the updated pickled data for the session.
-        json j;
-        j["session"] = pickle(ptr.get(), SECRET);
+    // Save the updated pickled data for the session.
+    json j;
+    j["session"] = pickle(ptr.get(), pickle_secret_);
 
-        auto txn = lmdb::txn::begin(env_);
-        outboundMegolmSessionDb_.put(txn, room_id, j.dump());
-        megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    outboundMegolmSessionDb_.put(txn, room_id, j.dump());
+    megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
+    txn.commit();
 }
 
 void
 Cache::dropOutboundMegolmSession(const std::string &room_id)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        if (!outboundMegolmSessionExists(room_id))
-                return;
+    if (!outboundMegolmSessionExists(room_id))
+        return;
 
-        {
-                auto txn = lmdb::txn::begin(env_);
-                outboundMegolmSessionDb_.del(txn, room_id);
-                // don't delete session data, so that we can still share the session.
-                txn.commit();
-        }
+    {
+        auto txn = lmdb::txn::begin(env_);
+        outboundMegolmSessionDb_.del(txn, room_id);
+        // don't delete session data, so that we can still share the session.
+        txn.commit();
+    }
 }
 
 void
@@ -529,86 +533,86 @@ Cache::saveOutboundMegolmSession(const std::string &room_id,
                                  const GroupSessionData &data_,
                                  mtx::crypto::OutboundGroupSessionPtr &session)
 {
-        using namespace mtx::crypto;
-        const auto pickled = pickle(session.get(), SECRET);
+    using namespace mtx::crypto;
+    const auto pickled = pickle(session.get(), pickle_secret_);
 
-        GroupSessionData data = data_;
-        data.message_index    = olm_outbound_group_session_message_index(session.get());
-        MegolmSessionIndex index;
-        index.room_id    = room_id;
-        index.sender_key = olm::client()->identity_keys().ed25519;
-        index.session_id = mtx::crypto::session_id(session.get());
+    GroupSessionData data = data_;
+    data.message_index    = olm_outbound_group_session_message_index(session.get());
+    MegolmSessionIndex index;
+    index.room_id    = room_id;
+    index.sender_key = olm::client()->identity_keys().ed25519;
+    index.session_id = mtx::crypto::session_id(session.get());
 
-        json j;
-        j["session"] = pickled;
+    json j;
+    j["session"] = pickled;
 
-        auto txn = lmdb::txn::begin(env_);
-        outboundMegolmSessionDb_.put(txn, room_id, j.dump());
-        megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    outboundMegolmSessionDb_.put(txn, room_id, j.dump());
+    megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
+    txn.commit();
 }
 
 bool
 Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
 {
-        try {
-                auto txn = ro_txn(env_);
-                std::string_view value;
-                return outboundMegolmSessionDb_.get(txn, room_id, value);
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
-                return false;
-        }
+    try {
+        auto txn = ro_txn(env_);
+        std::string_view value;
+        return outboundMegolmSessionDb_.get(txn, room_id, value);
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
+        return false;
+    }
 }
 
 OutboundGroupSessionDataRef
 Cache::getOutboundMegolmSession(const std::string &room_id)
 {
-        try {
-                using namespace mtx::crypto;
+    try {
+        using namespace mtx::crypto;
 
-                auto txn = ro_txn(env_);
-                std::string_view value;
-                outboundMegolmSessionDb_.get(txn, room_id, value);
-                auto obj = json::parse(value);
+        auto txn = ro_txn(env_);
+        std::string_view value;
+        outboundMegolmSessionDb_.get(txn, room_id, value);
+        auto obj = json::parse(value);
 
-                OutboundGroupSessionDataRef ref{};
-                ref.session = unpickle(obj.at("session"), SECRET);
+        OutboundGroupSessionDataRef ref{};
+        ref.session = unpickle(obj.at("session"), pickle_secret_);
 
-                MegolmSessionIndex index;
-                index.room_id    = room_id;
-                index.sender_key = olm::client()->identity_keys().ed25519;
-                index.session_id = mtx::crypto::session_id(ref.session.get());
+        MegolmSessionIndex index;
+        index.room_id    = room_id;
+        index.sender_key = olm::client()->identity_keys().ed25519;
+        index.session_id = mtx::crypto::session_id(ref.session.get());
 
-                if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
-                        ref.data = nlohmann::json::parse(value).get();
-                }
-
-                return ref;
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
-                return {};
+        if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
+            ref.data = nlohmann::json::parse(value).get();
         }
+
+        return ref;
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
+        return {};
+    }
 }
 
 std::optional
 Cache::getMegolmSessionData(const MegolmSessionIndex &index)
 {
-        try {
-                using namespace mtx::crypto;
+    try {
+        using namespace mtx::crypto;
 
-                auto txn = ro_txn(env_);
+        auto txn = ro_txn(env_);
 
-                std::string_view value;
-                if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
-                        return nlohmann::json::parse(value).get();
-                }
-
-                return std::nullopt;
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what());
-                return std::nullopt;
+        std::string_view value;
+        if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
+            return nlohmann::json::parse(value).get();
         }
+
+        return std::nullopt;
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what());
+        return std::nullopt;
+    }
 }
 //
 // OLM sessions.
@@ -619,287 +623,353 @@ Cache::saveOlmSession(const std::string &curve25519,
                       mtx::crypto::OlmSessionPtr session,
                       uint64_t timestamp)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        const auto pickled    = pickle(session.get(), SECRET);
-        const auto session_id = mtx::crypto::session_id(session.get());
+    const auto pickled    = pickle(session.get(), pickle_secret_);
+    const auto session_id = mtx::crypto::session_id(session.get());
 
-        StoredOlmSession stored_session;
-        stored_session.pickled_session = pickled;
-        stored_session.last_message_ts = timestamp;
+    StoredOlmSession stored_session;
+    stored_session.pickled_session = pickled;
+    stored_session.last_message_ts = timestamp;
 
-        db.put(txn, session_id, json(stored_session).dump());
+    db.put(txn, session_id, json(stored_session).dump());
 
-        txn.commit();
+    txn.commit();
 }
 
 std::optional
 Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        std::string_view pickled;
-        bool found = db.get(txn, session_id, pickled);
+    std::string_view pickled;
+    bool found = db.get(txn, session_id, pickled);
 
-        txn.commit();
+    txn.commit();
 
-        if (found) {
-                auto data = json::parse(pickled).get();
-                return unpickle(data.pickled_session, SECRET);
-        }
+    if (found) {
+        auto data = json::parse(pickled).get();
+        return unpickle(data.pickled_session, pickle_secret_);
+    }
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 std::optional
 Cache::getLatestOlmSession(const std::string &curve25519)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        std::string_view session_id, pickled_session;
+    std::string_view session_id, pickled_session;
 
-        std::optional currentNewest;
+    std::optional currentNewest;
 
-        auto cursor = lmdb::cursor::open(txn, db);
-        while (cursor.get(session_id, pickled_session, MDB_NEXT)) {
-                auto data = json::parse(pickled_session).get();
-                if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts)
-                        currentNewest = data;
-        }
-        cursor.close();
+    auto cursor = lmdb::cursor::open(txn, db);
+    while (cursor.get(session_id, pickled_session, MDB_NEXT)) {
+        auto data = json::parse(pickled_session).get();
+        if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts)
+            currentNewest = data;
+    }
+    cursor.close();
 
-        txn.commit();
+    txn.commit();
 
-        return currentNewest
-                 ? std::optional(unpickle(currentNewest->pickled_session, SECRET))
-                 : std::nullopt;
+    return currentNewest ? std::optional(unpickle(currentNewest->pickled_session,
+                                                                 pickle_secret_))
+                         : std::nullopt;
 }
 
 std::vector
 Cache::getOlmSessions(const std::string &curve25519)
 {
-        using namespace mtx::crypto;
+    using namespace mtx::crypto;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getOlmSessionsDb(txn, curve25519);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getOlmSessionsDb(txn, curve25519);
 
-        std::string_view session_id, unused;
-        std::vector res;
+    std::string_view session_id, unused;
+    std::vector res;
 
-        auto cursor = lmdb::cursor::open(txn, db);
-        while (cursor.get(session_id, unused, MDB_NEXT))
-                res.emplace_back(session_id);
-        cursor.close();
+    auto cursor = lmdb::cursor::open(txn, db);
+    while (cursor.get(session_id, unused, MDB_NEXT))
+        res.emplace_back(session_id);
+    cursor.close();
 
-        txn.commit();
+    txn.commit();
 
-        return res;
+    return res;
 }
 
 void
 Cache::saveOlmAccount(const std::string &data)
 {
-        auto txn = lmdb::txn::begin(env_);
-        syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data);
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data);
+    txn.commit();
 }
 
 std::string
 Cache::restoreOlmAccount()
 {
+    auto txn = ro_txn(env_);
+
+    std::string_view pickled;
+    syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled);
+
+    return std::string(pickled.data(), pickled.size());
+}
+
+void
+Cache::saveBackupVersion(const OnlineBackupVersion &data)
+{
+    auto txn = lmdb::txn::begin(env_);
+    syncStateDb_.put(txn, CURRENT_ONLINE_BACKUP_VERSION, nlohmann::json(data).dump());
+    txn.commit();
+}
+
+void
+Cache::deleteBackupVersion()
+{
+    auto txn = lmdb::txn::begin(env_);
+    syncStateDb_.del(txn, CURRENT_ONLINE_BACKUP_VERSION);
+    txn.commit();
+}
+
+std::optional
+Cache::backupVersion()
+{
+    try {
         auto txn = ro_txn(env_);
-        std::string_view pickled;
-        syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled);
+        std::string_view v;
+        syncStateDb_.get(txn, CURRENT_ONLINE_BACKUP_VERSION, v);
 
-        return std::string(pickled.data(), pickled.size());
+        return nlohmann::json::parse(v).get();
+    } catch (...) {
+        return std::nullopt;
+    }
+}
+
+static void
+fatalSecretError()
+{
+    QMessageBox::critical(
+      ChatPage::instance(),
+      QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"),
+      QCoreApplication::translate(
+        "SecretStorage",
+        "Nheko could not connect to the secure storage to save encryption secrets to. This can "
+        "have multiple reasons. Check if your D-Bus service is running and you have configured a "
+        "service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If "
+        "you are having trouble, feel free to open an issue here: "
+        "https://github.com/Nheko-Reborn/nheko/issues"));
+
+    QCoreApplication::exit(1);
+    exit(1);
 }
 
 void
-Cache::storeSecret(const std::string name, const std::string secret)
+Cache::storeSecret(const std::string name, const std::string secret, bool internal)
 {
-        auto settings = UserSettings::instance();
-        auto job      = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
-        job->setAutoDelete(true);
-        job->setInsecureFallback(true);
-        job->setSettings(UserSettings::instance()->qsettings());
+    auto settings = UserSettings::instance();
+    auto job      = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
+    job->setAutoDelete(true);
+    job->setInsecureFallback(true);
+    job->setSettings(UserSettings::instance()->qsettings());
 
-        job->setKey(
-          "matrix." +
-          QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
-                    .toBase64()) +
-          "." + QString::fromStdString(name));
+    job->setKey(
+      (internal ? "nheko." : "matrix.") +
+      QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
+                .toBase64()) +
+      "." + QString::fromStdString(name));
 
-        job->setTextData(QString::fromStdString(secret));
+    job->setTextData(QString::fromStdString(secret));
 
-        QObject::connect(
-          job,
-          &QKeychain::WritePasswordJob::finished,
-          this,
-          [name, this](QKeychain::Job *job) {
-                  if (job->error()) {
-                          nhlog::db()->warn("Storing secret '{}' failed: {}",
-                                            name,
-                                            job->errorString().toStdString());
-                  } else {
-                          // if we emit the signal directly, qtkeychain breaks and won't execute new
-                          // jobs. You can't start a job from the finish signal of a job.
-                          QTimer::singleShot(100, [this, name] { emit secretChanged(name); });
-                          nhlog::db()->info("Storing secret '{}' successful", name);
-                  }
-          },
-          Qt::ConnectionType::DirectConnection);
-        job->start();
+    QObject::connect(
+      job,
+      &QKeychain::WritePasswordJob::finished,
+      this,
+      [name, this](QKeychain::Job *job) {
+          if (job->error()) {
+              nhlog::db()->warn(
+                "Storing secret '{}' failed: {}", name, job->errorString().toStdString());
+              fatalSecretError();
+          } else {
+              // if we emit the signal directly, qtkeychain breaks and won't execute new
+              // jobs. You can't start a job from the finish signal of a job.
+              QTimer::singleShot(100, [this, name] { emit secretChanged(name); });
+              nhlog::db()->info("Storing secret '{}' successful", name);
+          }
+      },
+      Qt::ConnectionType::DirectConnection);
+    job->start();
 }
 
 void
-Cache::deleteSecret(const std::string name)
+Cache::deleteSecret(const std::string name, bool internal)
 {
-        auto settings = UserSettings::instance();
-        QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
-        job.setAutoDelete(false);
-        job.setInsecureFallback(true);
-        job.setSettings(UserSettings::instance()->qsettings());
+    auto settings = UserSettings::instance();
+    QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
+    job.setAutoDelete(false);
+    job.setInsecureFallback(true);
+    job.setSettings(UserSettings::instance()->qsettings());
 
-        job.setKey(
-          "matrix." +
-          QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
-                    .toBase64()) +
-          "." + QString::fromStdString(name));
+    job.setKey(
+      (internal ? "nheko." : "matrix.") +
+      QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
+                .toBase64()) +
+      "." + QString::fromStdString(name));
 
-        // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
-        // time!
-        QEventLoop loop;
-        job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
-        job.start();
-        loop.exec();
+    // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
+    // time!
+    QEventLoop loop;
+    job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+    job.start();
+    loop.exec();
 
-        emit secretChanged(name);
+    emit secretChanged(name);
 }
 
 std::optional
-Cache::secret(const std::string name)
+Cache::secret(const std::string name, bool internal)
 {
-        auto settings = UserSettings::instance();
-        QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
-        job.setAutoDelete(false);
-        job.setInsecureFallback(true);
-        job.setSettings(UserSettings::instance()->qsettings());
+    auto settings = UserSettings::instance();
+    QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
+    job.setAutoDelete(false);
+    job.setInsecureFallback(true);
+    job.setSettings(UserSettings::instance()->qsettings());
 
-        job.setKey(
-          "matrix." +
-          QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
-                    .toBase64()) +
-          "." + QString::fromStdString(name));
+    job.setKey(
+      (internal ? "nheko." : "matrix.") +
+      QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
+                .toBase64()) +
+      "." + QString::fromStdString(name));
 
-        // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
-        // time!
-        QEventLoop loop;
-        job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
-        job.start();
-        loop.exec();
+    // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
+    // time!
+    QEventLoop loop;
+    job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+    job.start();
+    loop.exec();
 
-        const QString secret = job.textData();
-        if (job.error()) {
-                nhlog::db()->debug(
-                  "Restoring secret '{}' failed: {}", name, job.errorString().toStdString());
-                return std::nullopt;
-        }
-        if (secret.isEmpty()) {
-                nhlog::db()->debug("Restored empty secret '{}'.", name);
-                return std::nullopt;
+    const QString secret = job.textData();
+    if (job.error()) {
+        if (job.error() == QKeychain::Error::EntryNotFound)
+            return std::nullopt;
+        nhlog::db()->error("Restoring secret '{}' failed ({}): {}",
+                           name,
+                           job.error(),
+                           job.errorString().toStdString());
+
+        fatalSecretError();
+        return std::nullopt;
+    }
+    if (secret.isEmpty()) {
+        nhlog::db()->debug("Restored empty secret '{}'.", name);
+        return std::nullopt;
+    }
+
+    return secret.toStdString();
+}
+
+std::string
+Cache::pickleSecret()
+{
+    if (pickle_secret_.empty()) {
+        auto s = secret("pickle_secret", true);
+        if (!s) {
+            this->pickle_secret_ = mtx::client::utils::random_token(64, true);
+            storeSecret("pickle_secret", pickle_secret_, true);
+        } else {
+            this->pickle_secret_ = *s;
         }
+    }
 
-        return secret.toStdString();
+    return pickle_secret_;
 }
 
 void
 Cache::removeInvite(lmdb::txn &txn, const std::string &room_id)
 {
-        invitesDb_.del(txn, room_id);
-        getInviteStatesDb(txn, room_id).drop(txn, true);
-        getInviteMembersDb(txn, room_id).drop(txn, true);
+    invitesDb_.del(txn, room_id);
+    getInviteStatesDb(txn, room_id).drop(txn, true);
+    getInviteMembersDb(txn, room_id).drop(txn, true);
 }
 
 void
 Cache::removeInvite(const std::string &room_id)
 {
-        auto txn = lmdb::txn::begin(env_);
-        removeInvite(txn, room_id);
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    removeInvite(txn, room_id);
+    txn.commit();
 }
 
 void
 Cache::removeRoom(lmdb::txn &txn, const std::string &roomid)
 {
-        roomsDb_.del(txn, roomid);
-        getStatesDb(txn, roomid).drop(txn, true);
-        getAccountDataDb(txn, roomid).drop(txn, true);
-        getMembersDb(txn, roomid).drop(txn, true);
+    roomsDb_.del(txn, roomid);
+    getStatesDb(txn, roomid).drop(txn, true);
+    getAccountDataDb(txn, roomid).drop(txn, true);
+    getMembersDb(txn, roomid).drop(txn, true);
 }
 
 void
 Cache::removeRoom(const std::string &roomid)
 {
-        auto txn = lmdb::txn::begin(env_, nullptr, 0);
-        roomsDb_.del(txn, roomid);
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_, nullptr, 0);
+    roomsDb_.del(txn, roomid);
+    txn.commit();
 }
 
 void
 Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token)
 {
-        syncStateDb_.put(txn, NEXT_BATCH_KEY, token);
-}
-
-void
-Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
-{
-        setNextBatchToken(txn, token.toStdString());
+    syncStateDb_.put(txn, NEXT_BATCH_KEY, token);
 }
 
 bool
 Cache::isInitialized()
 {
-        if (!env_.handle())
-                return false;
+    if (!env_.handle())
+        return false;
 
-        auto txn = ro_txn(env_);
-        std::string_view token;
+    auto txn = ro_txn(env_);
+    std::string_view token;
 
-        bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
+    bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
 
-        return res;
+    return res;
 }
 
 std::string
 Cache::nextBatchToken()
 {
-        if (!env_.handle())
-                throw lmdb::error("Env already closed", MDB_INVALID);
+    if (!env_.handle())
+        throw lmdb::error("Env already closed", MDB_INVALID);
 
-        auto txn = ro_txn(env_);
-        std::string_view token;
+    auto txn = ro_txn(env_);
+    std::string_view token;
 
-        bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
+    bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
 
-        if (result)
-                return std::string(token.data(), token.size());
-        else
-                return "";
+    if (result)
+        return std::string(token.data(), token.size());
+    else
+        return "";
 }
 
 void
 Cache::deleteData()
 {
+    if (this->databaseReady_) {
         this->databaseReady_ = false;
         // TODO: We need to remove the env_ while not accepting new requests.
         lmdb::dbi_close(env_, syncStateDb_);
@@ -920,596 +990,607 @@ Cache::deleteData()
         verification_storage.status.clear();
 
         if (!cacheDirectory_.isEmpty()) {
-                QDir(cacheDirectory_).removeRecursively();
-                nhlog::db()->info("deleted cache files from disk");
+            QDir(cacheDirectory_).removeRecursively();
+            nhlog::db()->info("deleted cache files from disk");
         }
 
         deleteSecret(mtx::secret_storage::secrets::megolm_backup_v1);
         deleteSecret(mtx::secret_storage::secrets::cross_signing_master);
         deleteSecret(mtx::secret_storage::secrets::cross_signing_user_signing);
         deleteSecret(mtx::secret_storage::secrets::cross_signing_self_signing);
+        deleteSecret("pickle_secret", true);
+    }
 }
 
 //! migrates db to the current format
 bool
 Cache::runMigrations()
 {
-        std::string stored_version;
-        {
-                auto txn = ro_txn(env_);
-
-                std::string_view current_version;
-                bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version);
-
-                if (!res)
-                        return false;
-
-                stored_version = std::string(current_version);
-        }
-
-        std::vector>> migrations{
-          {"2020.05.01",
-           [this]() {
-                   try {
-                           auto txn = lmdb::txn::begin(env_, nullptr);
-                           auto pending_receipts =
-                             lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
-                           lmdb::dbi_drop(txn, pending_receipts, true);
-                           txn.commit();
-                   } catch (const lmdb::error &) {
-                           nhlog::db()->critical(
-                             "Failed to delete pending_receipts database in migration!");
-                           return false;
-                   }
-
-                   nhlog::db()->info("Successfully deleted pending receipts database.");
-                   return true;
-           }},
-          {"2020.07.05",
-           [this]() {
-                   try {
-                           auto txn      = lmdb::txn::begin(env_, nullptr);
-                           auto room_ids = getRoomIds(txn);
-
-                           for (const auto &room_id : room_ids) {
-                                   try {
-                                           auto messagesDb = lmdb::dbi::open(
-                                             txn, std::string(room_id + "/messages").c_str());
-
-                                           // keep some old messages and batch token
-                                           {
-                                                   auto roomsCursor =
-                                                     lmdb::cursor::open(txn, messagesDb);
-                                                   std::string_view ts, stored_message;
-                                                   bool start = true;
-                                                   mtx::responses::Timeline oldMessages;
-                                                   while (roomsCursor.get(ts,
-                                                                          stored_message,
-                                                                          start ? MDB_FIRST
-                                                                                : MDB_NEXT)) {
-                                                           start = false;
-
-                                                           auto j = json::parse(std::string_view(
-                                                             stored_message.data(),
-                                                             stored_message.size()));
-
-                                                           if (oldMessages.prev_batch.empty())
-                                                                   oldMessages.prev_batch =
-                                                                     j["token"].get();
-                                                           else if (j["token"] !=
-                                                                    oldMessages.prev_batch)
-                                                                   break;
-
-                                                           mtx::events::collections::TimelineEvent
-                                                             te;
-                                                           mtx::events::collections::from_json(
-                                                             j["event"], te);
-                                                           oldMessages.events.push_back(te.data);
-                                                   }
-                                                   // messages were stored in reverse order, so we
-                                                   // need to reverse them
-                                                   std::reverse(oldMessages.events.begin(),
-                                                                oldMessages.events.end());
-                                                   // save messages using the new method
-                                                   auto eventsDb = getEventsDb(txn, room_id);
-                                                   saveTimelineMessages(
-                                                     txn, eventsDb, room_id, oldMessages);
-                                           }
-
-                                           // delete old messages db
-                                           lmdb::dbi_drop(txn, messagesDb, true);
-                                   } catch (std::exception &e) {
-                                           nhlog::db()->error(
-                                             "While migrating messages from {}, ignoring error {}",
-                                             room_id,
-                                             e.what());
-                                   }
-                           }
-                           txn.commit();
-                   } catch (const lmdb::error &) {
-                           nhlog::db()->critical(
-                             "Failed to delete messages database in migration!");
-                           return false;
-                   }
-
-                   nhlog::db()->info("Successfully deleted pending receipts database.");
-                   return true;
-           }},
-          {"2020.10.20",
-           [this]() {
-                   try {
-                           using namespace mtx::crypto;
-
-                           auto txn = lmdb::txn::begin(env_);
-
-                           auto mainDb = lmdb::dbi::open(txn, nullptr);
-
-                           std::string_view dbName, ignored;
-                           auto olmDbCursor = lmdb::cursor::open(txn, mainDb);
-                           while (olmDbCursor.get(dbName, ignored, MDB_NEXT)) {
-                                   // skip every db but olm session dbs
-                                   nhlog::db()->debug("Db {}", dbName);
-                                   if (dbName.find("olm_sessions/") != 0)
-                                           continue;
-
-                                   nhlog::db()->debug("Migrating {}", dbName);
-
-                                   auto olmDb = lmdb::dbi::open(txn, std::string(dbName).c_str());
-
-                                   std::string_view session_id, session_value;
-
-                                   std::vector> sessions;
-
-                                   auto cursor = lmdb::cursor::open(txn, olmDb);
-                                   while (cursor.get(session_id, session_value, MDB_NEXT)) {
-                                           nhlog::db()->debug("session_id {}, session_value {}",
-                                                              session_id,
-                                                              session_value);
-                                           StoredOlmSession session;
-                                           bool invalid = false;
-                                           for (auto c : session_value)
-                                                   if (!isprint(c)) {
-                                                           invalid = true;
-                                                           break;
-                                                   }
-                                           if (invalid)
-                                                   continue;
-
-                                           nhlog::db()->debug("Not skipped");
-
-                                           session.pickled_session = session_value;
-                                           sessions.emplace_back(session_id, session);
-                                   }
-                                   cursor.close();
-
-                                   olmDb.drop(txn, true);
-
-                                   auto newDbName = std::string(dbName);
-                                   newDbName.erase(0, sizeof("olm_sessions") - 1);
-                                   newDbName = "olm_sessions.v2" + newDbName;
-
-                                   auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE);
-
-                                   for (const auto &[key, value] : sessions) {
-                                           // nhlog::db()->debug("{}\n{}", key, json(value).dump());
-                                           newDb.put(txn, key, json(value).dump());
-                                   }
-                           }
-                           olmDbCursor.close();
-
-                           txn.commit();
-                   } catch (const lmdb::error &) {
-                           nhlog::db()->critical("Failed to migrate olm sessions,");
-                           return false;
-                   }
-
-                   nhlog::db()->info("Successfully migrated olm sessions.");
-                   return true;
-           }},
-        };
-
-        nhlog::db()->info("Running migrations, this may take a while!");
-        for (const auto &[target_version, migration] : migrations) {
-                if (target_version > stored_version)
-                        if (!migration()) {
-                                nhlog::db()->critical("migration failure!");
-                                return false;
-                        }
-        }
-        nhlog::db()->info("Migrations finished.");
-
-        setCurrentFormat();
-        return true;
-}
-
-cache::CacheVersion
-Cache::formatVersion()
-{
+    std::string stored_version;
+    {
         auto txn = ro_txn(env_);
 
         std::string_view current_version;
         bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version);
 
         if (!res)
-                return cache::CacheVersion::Older;
+            return false;
 
-        std::string stored_version(current_version.data(), current_version.size());
+        stored_version = std::string(current_version);
+    }
 
-        if (stored_version < CURRENT_CACHE_FORMAT_VERSION)
-                return cache::CacheVersion::Older;
-        else if (stored_version > CURRENT_CACHE_FORMAT_VERSION)
-                return cache::CacheVersion::Older;
-        else
-                return cache::CacheVersion::Current;
+    std::vector>> migrations{
+      {"2020.05.01",
+       [this]() {
+           try {
+               auto txn              = lmdb::txn::begin(env_, nullptr);
+               auto pending_receipts = lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
+               lmdb::dbi_drop(txn, pending_receipts, true);
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to delete pending_receipts database in migration!");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully deleted pending receipts database.");
+           return true;
+       }},
+      {"2020.07.05",
+       [this]() {
+           try {
+               auto txn      = lmdb::txn::begin(env_, nullptr);
+               auto room_ids = getRoomIds(txn);
+
+               for (const auto &room_id : room_ids) {
+                   try {
+                       auto messagesDb =
+                         lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str());
+
+                       // keep some old messages and batch token
+                       {
+                           auto roomsCursor = lmdb::cursor::open(txn, messagesDb);
+                           std::string_view ts, stored_message;
+                           bool start = true;
+                           mtx::responses::Timeline oldMessages;
+                           while (
+                             roomsCursor.get(ts, stored_message, start ? MDB_FIRST : MDB_NEXT)) {
+                               start = false;
+
+                               auto j = json::parse(
+                                 std::string_view(stored_message.data(), stored_message.size()));
+
+                               if (oldMessages.prev_batch.empty())
+                                   oldMessages.prev_batch = j["token"].get();
+                               else if (j["token"] != oldMessages.prev_batch)
+                                   break;
+
+                               mtx::events::collections::TimelineEvent te;
+                               mtx::events::collections::from_json(j["event"], te);
+                               oldMessages.events.push_back(te.data);
+                           }
+                           // messages were stored in reverse order, so we
+                           // need to reverse them
+                           std::reverse(oldMessages.events.begin(), oldMessages.events.end());
+                           // save messages using the new method
+                           auto eventsDb = getEventsDb(txn, room_id);
+                           saveTimelineMessages(txn, eventsDb, room_id, oldMessages);
+                       }
+
+                       // delete old messages db
+                       lmdb::dbi_drop(txn, messagesDb, true);
+                   } catch (std::exception &e) {
+                       nhlog::db()->error(
+                         "While migrating messages from {}, ignoring error {}", room_id, e.what());
+                   }
+               }
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to delete messages database in migration!");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully deleted pending receipts database.");
+           return true;
+       }},
+      {"2020.10.20",
+       [this]() {
+           try {
+               using namespace mtx::crypto;
+
+               auto txn = lmdb::txn::begin(env_);
+
+               auto mainDb = lmdb::dbi::open(txn, nullptr);
+
+               std::string_view dbName, ignored;
+               auto olmDbCursor = lmdb::cursor::open(txn, mainDb);
+               while (olmDbCursor.get(dbName, ignored, MDB_NEXT)) {
+                   // skip every db but olm session dbs
+                   nhlog::db()->debug("Db {}", dbName);
+                   if (dbName.find("olm_sessions/") != 0)
+                       continue;
+
+                   nhlog::db()->debug("Migrating {}", dbName);
+
+                   auto olmDb = lmdb::dbi::open(txn, std::string(dbName).c_str());
+
+                   std::string_view session_id, session_value;
+
+                   std::vector> sessions;
+
+                   auto cursor = lmdb::cursor::open(txn, olmDb);
+                   while (cursor.get(session_id, session_value, MDB_NEXT)) {
+                       nhlog::db()->debug(
+                         "session_id {}, session_value {}", session_id, session_value);
+                       StoredOlmSession session;
+                       bool invalid = false;
+                       for (auto c : session_value)
+                           if (!isprint(c)) {
+                               invalid = true;
+                               break;
+                           }
+                       if (invalid)
+                           continue;
+
+                       nhlog::db()->debug("Not skipped");
+
+                       session.pickled_session = session_value;
+                       sessions.emplace_back(session_id, session);
+                   }
+                   cursor.close();
+
+                   olmDb.drop(txn, true);
+
+                   auto newDbName = std::string(dbName);
+                   newDbName.erase(0, sizeof("olm_sessions") - 1);
+                   newDbName = "olm_sessions.v2" + newDbName;
+
+                   auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE);
+
+                   for (const auto &[key, value] : sessions) {
+                       // nhlog::db()->debug("{}\n{}", key, json(value).dump());
+                       newDb.put(txn, key, json(value).dump());
+                   }
+               }
+               olmDbCursor.close();
+
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to migrate olm sessions,");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully migrated olm sessions.");
+           return true;
+       }},
+      {"2021.08.22",
+       [this]() {
+           try {
+               auto txn      = lmdb::txn::begin(env_, nullptr);
+               auto try_drop = [&txn](const std::string &dbName) {
+                   try {
+                       auto db = lmdb::dbi::open(txn, dbName.c_str());
+                       db.drop(txn, true);
+                   } catch (std::exception &e) {
+                       nhlog::db()->warn("Failed to drop '{}': {}", dbName, e.what());
+                   }
+               };
+
+               auto room_ids = getRoomIds(txn);
+
+               for (const auto &room : room_ids) {
+                   try_drop(room + "/state");
+                   try_drop(room + "/state_by_key");
+                   try_drop(room + "/account_data");
+                   try_drop(room + "/members");
+                   try_drop(room + "/mentions");
+                   try_drop(room + "/events");
+                   try_drop(room + "/event_order");
+                   try_drop(room + "/event2order");
+                   try_drop(room + "/msg2order");
+                   try_drop(room + "/order2msg");
+                   try_drop(room + "/pending");
+                   try_drop(room + "/related");
+               }
+
+               // clear db, don't delete
+               roomsDb_.drop(txn, false);
+               setNextBatchToken(txn, "");
+
+               txn.commit();
+           } catch (const lmdb::error &) {
+               nhlog::db()->critical("Failed to clear cache!");
+               return false;
+           }
+
+           nhlog::db()->info("Successfully cleared the cache. Will do a clean sync after startup.");
+           return true;
+       }},
+      {"2021.08.31",
+       [this]() {
+           storeSecret("pickle_secret", "secret", true);
+           return true;
+       }},
+    };
+
+    nhlog::db()->info("Running migrations, this may take a while!");
+    for (const auto &[target_version, migration] : migrations) {
+        if (target_version > stored_version)
+            if (!migration()) {
+                nhlog::db()->critical("migration failure!");
+                return false;
+            }
+    }
+    nhlog::db()->info("Migrations finished.");
+
+    setCurrentFormat();
+    return true;
+}
+
+cache::CacheVersion
+Cache::formatVersion()
+{
+    auto txn = ro_txn(env_);
+
+    std::string_view current_version;
+    bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version);
+
+    if (!res)
+        return cache::CacheVersion::Older;
+
+    std::string stored_version(current_version.data(), current_version.size());
+
+    if (stored_version < CURRENT_CACHE_FORMAT_VERSION)
+        return cache::CacheVersion::Older;
+    else if (stored_version > CURRENT_CACHE_FORMAT_VERSION)
+        return cache::CacheVersion::Older;
+    else
+        return cache::CacheVersion::Current;
 }
 
 void
 Cache::setCurrentFormat()
 {
-        auto txn = lmdb::txn::begin(env_);
+    auto txn = lmdb::txn::begin(env_);
 
-        syncStateDb_.put(txn, CACHE_FORMAT_VERSION_KEY, CURRENT_CACHE_FORMAT_VERSION);
+    syncStateDb_.put(txn, CACHE_FORMAT_VERSION_KEY, CURRENT_CACHE_FORMAT_VERSION);
 
-        txn.commit();
+    txn.commit();
 }
 
 CachedReceipts
 Cache::readReceipts(const QString &event_id, const QString &room_id)
 {
-        CachedReceipts receipts;
+    CachedReceipts receipts;
 
-        ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
-        nlohmann::json json_key = receipt_key;
+    ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()};
+    nlohmann::json json_key = receipt_key;
 
-        try {
-                auto txn = ro_txn(env_);
-                auto key = json_key.dump();
+    try {
+        auto txn = ro_txn(env_);
+        auto key = json_key.dump();
 
-                std::string_view value;
+        std::string_view value;
 
-                bool res = readReceiptsDb_.get(txn, key, value);
+        bool res = readReceiptsDb_.get(txn, key, value);
 
-                if (res) {
-                        auto json_response =
-                          json::parse(std::string_view(value.data(), value.size()));
-                        auto values = json_response.get>();
+        if (res) {
+            auto json_response = json::parse(std::string_view(value.data(), value.size()));
+            auto values        = json_response.get>();
 
-                        for (const auto &v : values)
-                                // timestamp, user_id
-                                receipts.emplace(v.second, v.first);
-                }
-
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("readReceipts: {}", e.what());
+            for (const auto &v : values)
+                // timestamp, user_id
+                receipts.emplace(v.second, v.first);
         }
 
-        return receipts;
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("readReceipts: {}", e.what());
+    }
+
+    return receipts;
 }
 
 void
 Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
 {
-        auto user_id = this->localUserId_.toStdString();
-        for (const auto &receipt : receipts) {
-                const auto event_id = receipt.first;
-                auto event_receipts = receipt.second;
+    auto user_id = this->localUserId_.toStdString();
+    for (const auto &receipt : receipts) {
+        const auto event_id = receipt.first;
+        auto event_receipts = receipt.second;
 
-                ReadReceiptKey receipt_key{event_id, room_id};
-                nlohmann::json json_key = receipt_key;
+        ReadReceiptKey receipt_key{event_id, room_id};
+        nlohmann::json json_key = receipt_key;
 
-                try {
-                        const auto key = json_key.dump();
+        try {
+            const auto key = json_key.dump();
 
-                        std::string_view prev_value;
+            std::string_view prev_value;
 
-                        bool exists = readReceiptsDb_.get(txn, key, prev_value);
+            bool exists = readReceiptsDb_.get(txn, key, prev_value);
 
-                        std::map saved_receipts;
+            std::map saved_receipts;
 
-                        // If an entry for the event id already exists, we would
-                        // merge the existing receipts with the new ones.
-                        if (exists) {
-                                auto json_value = json::parse(
-                                  std::string_view(prev_value.data(), prev_value.size()));
+            // If an entry for the event id already exists, we would
+            // merge the existing receipts with the new ones.
+            if (exists) {
+                auto json_value =
+                  json::parse(std::string_view(prev_value.data(), prev_value.size()));
 
-                                // Retrieve the saved receipts.
-                                saved_receipts = json_value.get>();
-                        }
+                // Retrieve the saved receipts.
+                saved_receipts = json_value.get>();
+            }
 
-                        // Append the new ones.
-                        for (const auto &[read_by, timestamp] : event_receipts) {
-                                if (read_by == user_id) {
-                                        emit removeNotification(QString::fromStdString(room_id),
-                                                                QString::fromStdString(event_id));
-                                }
-                                saved_receipts.emplace(read_by, timestamp);
-                        }
-
-                        // Save back the merged (or only the new) receipts.
-                        nlohmann::json json_updated_value = saved_receipts;
-                        std::string merged_receipts       = json_updated_value.dump();
-
-                        readReceiptsDb_.put(txn, key, merged_receipts);
-
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->critical("updateReadReceipts: {}", e.what());
+            // Append the new ones.
+            for (const auto &[read_by, timestamp] : event_receipts) {
+                if (read_by == user_id) {
+                    emit removeNotification(QString::fromStdString(room_id),
+                                            QString::fromStdString(event_id));
                 }
+                saved_receipts.emplace(read_by, timestamp);
+            }
+
+            // Save back the merged (or only the new) receipts.
+            nlohmann::json json_updated_value = saved_receipts;
+            std::string merged_receipts       = json_updated_value.dump();
+
+            readReceiptsDb_.put(txn, key, merged_receipts);
+
+        } catch (const lmdb::error &e) {
+            nhlog::db()->critical("updateReadReceipts: {}", e.what());
         }
+    }
 }
 
 void
 Cache::calculateRoomReadStatus()
 {
-        const auto joined_rooms = joinedRooms();
+    const auto joined_rooms = joinedRooms();
 
-        std::map readStatus;
+    std::map readStatus;
 
-        for (const auto &room : joined_rooms)
-                readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room));
+    for (const auto &room : joined_rooms)
+        readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room));
 
-        emit roomReadStatus(readStatus);
+    emit roomReadStatus(readStatus);
 }
 
 bool
 Cache::calculateRoomReadStatus(const std::string &room_id)
 {
-        std::string last_event_id_, fullyReadEventId_;
-        {
-                auto txn = ro_txn(env_);
+    std::string last_event_id_, fullyReadEventId_;
+    {
+        auto txn = ro_txn(env_);
 
-                // Get last event id on the room.
-                const auto last_event_id = getLastEventId(txn, room_id);
-                const auto localUser     = utils::localUser().toStdString();
+        // Get last event id on the room.
+        const auto last_event_id = getLastEventId(txn, room_id);
+        const auto localUser     = utils::localUser().toStdString();
 
-                std::string fullyReadEventId;
-                if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) {
-                        if (auto fr = std::get_if<
-                              mtx::events::AccountDataEvent>(
-                              &ev.value())) {
-                                fullyReadEventId = fr->content.event_id;
-                        }
-                }
-
-                if (last_event_id.empty() || fullyReadEventId.empty())
-                        return true;
-
-                if (last_event_id == fullyReadEventId)
-                        return false;
-
-                last_event_id_    = std::string(last_event_id);
-                fullyReadEventId_ = std::string(fullyReadEventId);
+        std::string fullyReadEventId;
+        if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) {
+            if (auto fr =
+                  std::get_if>(
+                    &ev.value())) {
+                fullyReadEventId = fr->content.event_id;
+            }
         }
 
-        // Retrieve all read receipts for that event.
-        return getEventIndex(room_id, last_event_id_) > getEventIndex(room_id, fullyReadEventId_);
+        if (last_event_id.empty() || fullyReadEventId.empty())
+            return true;
+
+        if (last_event_id == fullyReadEventId)
+            return false;
+
+        last_event_id_    = std::string(last_event_id);
+        fullyReadEventId_ = std::string(fullyReadEventId);
+    }
+
+    // Retrieve all read receipts for that event.
+    return getEventIndex(room_id, last_event_id_) > getEventIndex(room_id, fullyReadEventId_);
 }
 
 void
 Cache::saveState(const mtx::responses::Sync &res)
 {
-        using namespace mtx::events;
-        auto local_user_id = this->localUserId_.toStdString();
+    using namespace mtx::events;
+    auto local_user_id = this->localUserId_.toStdString();
 
-        auto currentBatchToken = nextBatchToken();
+    auto currentBatchToken = nextBatchToken();
 
-        auto txn = lmdb::txn::begin(env_);
+    auto txn = lmdb::txn::begin(env_);
 
-        setNextBatchToken(txn, res.next_batch);
+    setNextBatchToken(txn, res.next_batch);
 
-        if (!res.account_data.events.empty()) {
-                auto accountDataDb = getAccountDataDb(txn, "");
-                for (const auto &ev : res.account_data.events)
-                        std::visit(
-                          [&txn, &accountDataDb](const auto &event) {
-                                  auto j = json(event);
-                                  accountDataDb.put(txn, j["type"].get(), j.dump());
-                          },
-                          ev);
+    if (!res.account_data.events.empty()) {
+        auto accountDataDb = getAccountDataDb(txn, "");
+        for (const auto &ev : res.account_data.events)
+            std::visit(
+              [&txn, &accountDataDb](const auto &event) {
+                  auto j = json(event);
+                  accountDataDb.put(txn, j["type"].get(), j.dump());
+              },
+              ev);
+    }
+
+    auto userKeyCacheDb = getUserKeysDb(txn);
+
+    std::set spaces_with_updates;
+    std::set rooms_with_space_updates;
+
+    // Save joined rooms
+    for (const auto &room : res.rooms.join) {
+        auto statesdb    = getStatesDb(txn, room.first);
+        auto stateskeydb = getStatesKeyDb(txn, room.first);
+        auto membersdb   = getMembersDb(txn, room.first);
+        auto eventsDb    = getEventsDb(txn, room.first);
+
+        saveStateEvents(
+          txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.state.events);
+        saveStateEvents(
+          txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.timeline.events);
+
+        saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline);
+
+        RoomInfo updatedInfo;
+        updatedInfo.name       = getRoomName(txn, statesdb, membersdb).toStdString();
+        updatedInfo.topic      = getRoomTopic(txn, statesdb).toStdString();
+        updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
+        updatedInfo.version    = getRoomVersion(txn, statesdb).toStdString();
+        updatedInfo.is_space   = getRoomIsSpace(txn, statesdb);
+
+        if (updatedInfo.is_space) {
+            bool space_updates = false;
+            for (const auto &e : room.second.state.events)
+                if (std::holds_alternative>(e) ||
+                    std::holds_alternative>(e))
+                    space_updates = true;
+            for (const auto &e : room.second.timeline.events)
+                if (std::holds_alternative>(e) ||
+                    std::holds_alternative>(e))
+                    space_updates = true;
+
+            if (space_updates)
+                spaces_with_updates.insert(room.first);
         }
 
-        auto userKeyCacheDb = getUserKeysDb(txn);
-
-        std::set spaces_with_updates;
-        std::set rooms_with_space_updates;
-
-        // Save joined rooms
-        for (const auto &room : res.rooms.join) {
-                auto statesdb    = getStatesDb(txn, room.first);
-                auto stateskeydb = getStatesKeyDb(txn, room.first);
-                auto membersdb   = getMembersDb(txn, room.first);
-                auto eventsDb    = getEventsDb(txn, room.first);
-
-                saveStateEvents(txn,
-                                statesdb,
-                                stateskeydb,
-                                membersdb,
-                                eventsDb,
-                                room.first,
-                                room.second.state.events);
-                saveStateEvents(txn,
-                                statesdb,
-                                stateskeydb,
-                                membersdb,
-                                eventsDb,
-                                room.first,
-                                room.second.timeline.events);
-
-                saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline);
-
-                RoomInfo updatedInfo;
-                updatedInfo.name       = getRoomName(txn, statesdb, membersdb).toStdString();
-                updatedInfo.topic      = getRoomTopic(txn, statesdb).toStdString();
-                updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
-                updatedInfo.version    = getRoomVersion(txn, statesdb).toStdString();
-                updatedInfo.is_space   = getRoomIsSpace(txn, statesdb);
-
-                if (updatedInfo.is_space) {
-                        bool space_updates = false;
-                        for (const auto &e : room.second.state.events)
-                                if (std::holds_alternative>(e) ||
-                                    std::holds_alternative>(e))
-                                        space_updates = true;
-                        for (const auto &e : room.second.timeline.events)
-                                if (std::holds_alternative>(e) ||
-                                    std::holds_alternative>(e))
-                                        space_updates = true;
-
-                        if (space_updates)
-                                spaces_with_updates.insert(room.first);
+        {
+            bool room_has_space_update = false;
+            for (const auto &e : room.second.state.events) {
+                if (auto se = std::get_if>(&e)) {
+                    spaces_with_updates.insert(se->state_key);
+                    room_has_space_update = true;
                 }
-
-                {
-                        bool room_has_space_update = false;
-                        for (const auto &e : room.second.state.events) {
-                                if (auto se = std::get_if>(&e)) {
-                                        spaces_with_updates.insert(se->state_key);
-                                        room_has_space_update = true;
-                                }
-                        }
-                        for (const auto &e : room.second.timeline.events) {
-                                if (auto se = std::get_if>(&e)) {
-                                        spaces_with_updates.insert(se->state_key);
-                                        room_has_space_update = true;
-                                }
-                        }
-
-                        if (room_has_space_update)
-                                rooms_with_space_updates.insert(room.first);
+            }
+            for (const auto &e : room.second.timeline.events) {
+                if (auto se = std::get_if>(&e)) {
+                    spaces_with_updates.insert(se->state_key);
+                    room_has_space_update = true;
                 }
+            }
 
-                bool has_new_tags = false;
-                // Process the account_data associated with this room
-                if (!room.second.account_data.events.empty()) {
-                        auto accountDataDb = getAccountDataDb(txn, room.first);
-
-                        for (const auto &evt : room.second.account_data.events) {
-                                std::visit(
-                                  [&txn, &accountDataDb](const auto &event) {
-                                          auto j = json(event);
-                                          accountDataDb.put(
-                                            txn, j["type"].get(), j.dump());
-                                  },
-                                  evt);
-
-                                // for tag events
-                                if (std::holds_alternative>(
-                                      evt)) {
-                                        auto tags_evt =
-                                          std::get>(evt);
-                                        has_new_tags = true;
-                                        for (const auto &tag : tags_evt.content.tags) {
-                                                updatedInfo.tags.push_back(tag.first);
-                                        }
-                                }
-                                if (auto fr = std::get_if>(&evt)) {
-                                        nhlog::db()->debug("Fully read: {}", fr->content.event_id);
-                                }
-                        }
-                }
-                if (!has_new_tags) {
-                        // retrieve the old tags, they haven't changed
-                        std::string_view data;
-                        if (roomsDb_.get(txn, room.first, data)) {
-                                try {
-                                        RoomInfo tmp =
-                                          json::parse(std::string_view(data.data(), data.size()));
-                                        updatedInfo.tags = tmp.tags;
-                                } catch (const json::exception &e) {
-                                        nhlog::db()->warn(
-                                          "failed to parse room info: room_id ({}), {}: {}",
-                                          room.first,
-                                          std::string(data.data(), data.size()),
-                                          e.what());
-                                }
-                        }
-                }
-
-                roomsDb_.put(txn, room.first, json(updatedInfo).dump());
-
-                for (const auto &e : room.second.ephemeral.events) {
-                        if (auto receiptsEv = std::get_if<
-                              mtx::events::EphemeralEvent>(&e)) {
-                                Receipts receipts;
-
-                                for (const auto &[event_id, userReceipts] :
-                                     receiptsEv->content.receipts) {
-                                        for (const auto &[user_id, receipt] : userReceipts.users) {
-                                                receipts[event_id][user_id] = receipt.ts;
-                                        }
-                                }
-                                updateReadReceipt(txn, room.first, receipts);
-                        }
-                }
-
-                // Clean up non-valid invites.
-                removeInvite(txn, room.first);
+            if (room_has_space_update)
+                rooms_with_space_updates.insert(room.first);
         }
 
-        saveInvites(txn, res.rooms.invite);
+        bool has_new_tags = false;
+        // Process the account_data associated with this room
+        if (!room.second.account_data.events.empty()) {
+            auto accountDataDb = getAccountDataDb(txn, room.first);
 
-        savePresence(txn, res.presence);
+            for (const auto &evt : room.second.account_data.events) {
+                std::visit(
+                  [&txn, &accountDataDb](const auto &event) {
+                      auto j = json(event);
+                      accountDataDb.put(txn, j["type"].get(), j.dump());
+                  },
+                  evt);
 
-        markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken);
-        deleteUserKeys(txn, userKeyCacheDb, res.device_lists.left);
-
-        removeLeftRooms(txn, res.rooms.leave);
-
-        updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates));
-
-        txn.commit();
-
-        std::map readStatus;
-
-        for (const auto &room : res.rooms.join) {
-                for (const auto &e : room.second.ephemeral.events) {
-                        if (auto receiptsEv = std::get_if<
-                              mtx::events::EphemeralEvent>(&e)) {
-                                std::vector receipts;
-
-                                for (const auto &[event_id, userReceipts] :
-                                     receiptsEv->content.receipts) {
-                                        for (const auto &[user_id, receipt] : userReceipts.users) {
-                                                (void)receipt;
-
-                                                if (user_id != local_user_id) {
-                                                        receipts.push_back(
-                                                          QString::fromStdString(event_id));
-                                                        break;
-                                                }
-                                        }
-                                }
-                                if (!receipts.empty())
-                                        emit newReadReceipts(QString::fromStdString(room.first),
-                                                             receipts);
-                        }
+                // for tag events
+                if (std::holds_alternative>(evt)) {
+                    auto tags_evt = std::get>(evt);
+                    has_new_tags  = true;
+                    for (const auto &tag : tags_evt.content.tags) {
+                        updatedInfo.tags.push_back(tag.first);
+                    }
                 }
-                readStatus.emplace(QString::fromStdString(room.first),
-                                   calculateRoomReadStatus(room.first));
+                if (auto fr = std::get_if<
+                      mtx::events::AccountDataEvent>(&evt)) {
+                    nhlog::db()->debug("Fully read: {}", fr->content.event_id);
+                }
+            }
+        }
+        if (!has_new_tags) {
+            // retrieve the old tags, they haven't changed
+            std::string_view data;
+            if (roomsDb_.get(txn, room.first, data)) {
+                try {
+                    RoomInfo tmp     = json::parse(std::string_view(data.data(), data.size()));
+                    updatedInfo.tags = tmp.tags;
+                } catch (const json::exception &e) {
+                    nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
+                                      room.first,
+                                      std::string(data.data(), data.size()),
+                                      e.what());
+                }
+            }
         }
 
-        emit roomReadStatus(readStatus);
+        roomsDb_.put(txn, room.first, json(updatedInfo).dump());
+
+        for (const auto &e : room.second.ephemeral.events) {
+            if (auto receiptsEv =
+                  std::get_if>(&e)) {
+                Receipts receipts;
+
+                for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) {
+                    for (const auto &[user_id, receipt] : userReceipts.users) {
+                        receipts[event_id][user_id] = receipt.ts;
+                    }
+                }
+                updateReadReceipt(txn, room.first, receipts);
+            }
+        }
+
+        // Clean up non-valid invites.
+        removeInvite(txn, room.first);
+    }
+
+    saveInvites(txn, res.rooms.invite);
+
+    savePresence(txn, res.presence);
+
+    markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken);
+
+    removeLeftRooms(txn, res.rooms.leave);
+
+    updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates));
+
+    txn.commit();
+
+    std::map readStatus;
+
+    for (const auto &room : res.rooms.join) {
+        for (const auto &e : room.second.ephemeral.events) {
+            if (auto receiptsEv =
+                  std::get_if>(&e)) {
+                std::vector receipts;
+
+                for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) {
+                    for (const auto &[user_id, receipt] : userReceipts.users) {
+                        (void)receipt;
+
+                        if (user_id != local_user_id) {
+                            receipts.push_back(QString::fromStdString(event_id));
+                            break;
+                        }
+                    }
+                }
+                if (!receipts.empty())
+                    emit newReadReceipts(QString::fromStdString(room.first), receipts);
+            }
+        }
+        readStatus.emplace(QString::fromStdString(room.first), calculateRoomReadStatus(room.first));
+    }
+
+    emit roomReadStatus(readStatus);
 }
 
 void
 Cache::saveInvites(lmdb::txn &txn, const std::map &rooms)
 {
-        for (const auto &room : rooms) {
-                auto statesdb  = getInviteStatesDb(txn, room.first);
-                auto membersdb = getInviteMembersDb(txn, room.first);
+    for (const auto &room : rooms) {
+        auto statesdb  = getInviteStatesDb(txn, room.first);
+        auto membersdb = getInviteMembersDb(txn, room.first);
 
-                saveInvite(txn, statesdb, membersdb, room.second);
+        saveInvite(txn, statesdb, membersdb, room.second);
 
-                RoomInfo updatedInfo;
-                updatedInfo.name  = getInviteRoomName(txn, statesdb, membersdb).toStdString();
-                updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString();
-                updatedInfo.avatar_url =
-                  getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
-                updatedInfo.is_space  = getInviteRoomIsSpace(txn, statesdb);
-                updatedInfo.is_invite = true;
+        RoomInfo updatedInfo;
+        updatedInfo.name       = getInviteRoomName(txn, statesdb, membersdb).toStdString();
+        updatedInfo.topic      = getInviteRoomTopic(txn, statesdb).toStdString();
+        updatedInfo.avatar_url = getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
+        updatedInfo.is_space   = getInviteRoomIsSpace(txn, statesdb);
+        updatedInfo.is_invite  = true;
 
-                invitesDb_.put(txn, room.first, json(updatedInfo).dump());
-        }
+        invitesDb_.put(txn, room.first, json(updatedInfo).dump());
+    }
 }
 
 void
@@ -1518,32 +1599,29 @@ Cache::saveInvite(lmdb::txn &txn,
                   lmdb::dbi &membersdb,
                   const mtx::responses::InvitedRoom &room)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        for (const auto &e : room.invite_state) {
-                if (auto msg = std::get_if>(&e)) {
-                        auto display_name = msg->content.display_name.empty()
-                                              ? msg->state_key
-                                              : msg->content.display_name;
+    for (const auto &e : room.invite_state) {
+        if (auto msg = std::get_if>(&e)) {
+            auto display_name =
+              msg->content.display_name.empty() ? msg->state_key : msg->content.display_name;
 
-                        MemberInfo tmp{display_name, msg->content.avatar_url};
+            MemberInfo tmp{display_name, msg->content.avatar_url};
 
-                        membersdb.put(txn, msg->state_key, json(tmp).dump());
-                } else {
-                        std::visit(
-                          [&txn, &statesdb](auto msg) {
-                                  auto j = json(msg);
-                                  bool res =
-                                    statesdb.put(txn, j["type"].get(), j.dump());
+            membersdb.put(txn, msg->state_key, json(tmp).dump());
+        } else {
+            std::visit(
+              [&txn, &statesdb](auto msg) {
+                  auto j   = json(msg);
+                  bool res = statesdb.put(txn, j["type"].get(), j.dump());
 
-                                  if (!res)
-                                          nhlog::db()->warn("couldn't save data: {}",
-                                                            json(msg).dump());
-                          },
-                          e);
-                }
+                  if (!res)
+                      nhlog::db()->warn("couldn't save data: {}", json(msg).dump());
+              },
+              e);
         }
+    }
 }
 
 void
@@ -1551,281 +1629,279 @@ Cache::savePresence(
   lmdb::txn &txn,
   const std::vector> &presenceUpdates)
 {
-        for (const auto &update : presenceUpdates) {
-                auto presenceDb = getPresenceDb(txn);
+    for (const auto &update : presenceUpdates) {
+        auto presenceDb = getPresenceDb(txn);
 
-                presenceDb.put(txn, update.sender, json(update.content).dump());
-        }
+        presenceDb.put(txn, update.sender, json(update.content).dump());
+    }
 }
 
 std::vector
 Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
 {
-        std::vector rooms;
-        for (const auto &room : res.rooms.join) {
-                bool hasUpdates = false;
-                for (const auto &s : room.second.state.events) {
-                        if (containsStateUpdates(s)) {
-                                hasUpdates = true;
-                                break;
-                        }
-                }
-
-                for (const auto &s : room.second.timeline.events) {
-                        if (containsStateUpdates(s)) {
-                                hasUpdates = true;
-                                break;
-                        }
-                }
-
-                if (hasUpdates)
-                        rooms.emplace_back(room.first);
+    std::vector rooms;
+    for (const auto &room : res.rooms.join) {
+        bool hasUpdates = false;
+        for (const auto &s : room.second.state.events) {
+            if (containsStateUpdates(s)) {
+                hasUpdates = true;
+                break;
+            }
         }
 
-        for (const auto &room : res.rooms.invite) {
-                for (const auto &s : room.second.invite_state) {
-                        if (containsStateUpdates(s)) {
-                                rooms.emplace_back(room.first);
-                                break;
-                        }
-                }
+        for (const auto &s : room.second.timeline.events) {
+            if (containsStateUpdates(s)) {
+                hasUpdates = true;
+                break;
+            }
         }
 
-        return rooms;
+        if (hasUpdates)
+            rooms.emplace_back(room.first);
+    }
+
+    for (const auto &room : res.rooms.invite) {
+        for (const auto &s : room.second.invite_state) {
+            if (containsStateUpdates(s)) {
+                rooms.emplace_back(room.first);
+                break;
+            }
+        }
+    }
+
+    return rooms;
 }
 
 RoomInfo
 Cache::singleRoomInfo(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        try {
-                auto statesdb = getStatesDb(txn, room_id);
+    try {
+        auto statesdb = getStatesDb(txn, room_id);
 
-                std::string_view data;
+        std::string_view data;
 
-                // Check if the room is joined.
-                if (roomsDb_.get(txn, room_id, data)) {
-                        try {
-                                RoomInfo tmp     = json::parse(data);
-                                tmp.member_count = getMembersDb(txn, room_id).size(txn);
-                                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
-                                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
+        // Check if the room is joined.
+        if (roomsDb_.get(txn, room_id, data)) {
+            try {
+                RoomInfo tmp     = json::parse(data);
+                tmp.member_count = getMembersDb(txn, room_id).size(txn);
+                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
+                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
 
-                                return tmp;
-                        } catch (const json::exception &e) {
-                                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
-                                                  room_id,
-                                                  std::string(data.data(), data.size()),
-                                                  e.what());
-                        }
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn(
-                  "failed to read room info from db: room_id ({}), {}", room_id, e.what());
+                return tmp;
+            } catch (const json::exception &e) {
+                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
+                                  room_id,
+                                  std::string(data.data(), data.size()),
+                                  e.what());
+            }
         }
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("failed to read room info from db: room_id ({}), {}", room_id, e.what());
+    }
 
-        return RoomInfo();
+    return RoomInfo();
 }
 
 std::map
 Cache::getRoomInfo(const std::vector &rooms)
 {
-        std::map room_info;
+    std::map room_info;
 
-        // TODO This should be read only.
-        auto txn = lmdb::txn::begin(env_);
+    // TODO This should be read only.
+    auto txn = lmdb::txn::begin(env_);
 
-        for (const auto &room : rooms) {
-                std::string_view data;
-                auto statesdb = getStatesDb(txn, room);
+    for (const auto &room : rooms) {
+        std::string_view data;
+        auto statesdb = getStatesDb(txn, room);
 
-                // Check if the room is joined.
-                if (roomsDb_.get(txn, room, data)) {
-                        try {
-                                RoomInfo tmp     = json::parse(data);
-                                tmp.member_count = getMembersDb(txn, room).size(txn);
-                                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
-                                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
+        // Check if the room is joined.
+        if (roomsDb_.get(txn, room, data)) {
+            try {
+                RoomInfo tmp     = json::parse(data);
+                tmp.member_count = getMembersDb(txn, room).size(txn);
+                tmp.join_rule    = getRoomJoinRule(txn, statesdb);
+                tmp.guest_access = getRoomGuestAccess(txn, statesdb);
 
-                                room_info.emplace(QString::fromStdString(room), std::move(tmp));
-                        } catch (const json::exception &e) {
-                                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
-                                                  room,
-                                                  std::string(data.data(), data.size()),
-                                                  e.what());
-                        }
-                } else {
-                        // Check if the room is an invite.
-                        if (invitesDb_.get(txn, room, data)) {
-                                try {
-                                        RoomInfo tmp     = json::parse(std::string_view(data));
-                                        tmp.member_count = getInviteMembersDb(txn, room).size(txn);
+                room_info.emplace(QString::fromStdString(room), std::move(tmp));
+            } catch (const json::exception &e) {
+                nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
+                                  room,
+                                  std::string(data.data(), data.size()),
+                                  e.what());
+            }
+        } else {
+            // Check if the room is an invite.
+            if (invitesDb_.get(txn, room, data)) {
+                try {
+                    RoomInfo tmp     = json::parse(std::string_view(data));
+                    tmp.member_count = getInviteMembersDb(txn, room).size(txn);
 
-                                        room_info.emplace(QString::fromStdString(room),
-                                                          std::move(tmp));
-                                } catch (const json::exception &e) {
-                                        nhlog::db()->warn("failed to parse room info for invite: "
-                                                          "room_id ({}), {}: {}",
-                                                          room,
-                                                          std::string(data.data(), data.size()),
-                                                          e.what());
-                                }
-                        }
+                    room_info.emplace(QString::fromStdString(room), std::move(tmp));
+                } catch (const json::exception &e) {
+                    nhlog::db()->warn("failed to parse room info for invite: "
+                                      "room_id ({}), {}: {}",
+                                      room,
+                                      std::string(data.data(), data.size()),
+                                      e.what());
                 }
+            }
         }
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return room_info;
+    return room_info;
 }
 
 std::vector
 Cache::roomIds()
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector rooms;
-        std::string_view room_id, unused;
+    std::vector rooms;
+    std::string_view room_id, unused;
 
-        auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
-        while (roomsCursor.get(room_id, unused, MDB_NEXT))
-                rooms.push_back(QString::fromStdString(std::string(room_id)));
+    auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
+    while (roomsCursor.get(room_id, unused, MDB_NEXT))
+        rooms.push_back(QString::fromStdString(std::string(room_id)));
 
-        roomsCursor.close();
+    roomsCursor.close();
 
-        return rooms;
+    return rooms;
 }
 
 QMap
 Cache::getTimelineMentions()
 {
-        // TODO: Should be read-only, but getMentionsDb will attempt to create a DB
-        // if it doesn't exist, throwing an error.
-        auto txn = lmdb::txn::begin(env_, nullptr);
+    // TODO: Should be read-only, but getMentionsDb will attempt to create a DB
+    // if it doesn't exist, throwing an error.
+    auto txn = lmdb::txn::begin(env_, nullptr);
 
-        QMap notifs;
+    QMap notifs;
 
-        auto room_ids = getRoomIds(txn);
+    auto room_ids = getRoomIds(txn);
 
-        for (const auto &room_id : room_ids) {
-                auto roomNotifs                         = getTimelineMentionsForRoom(txn, room_id);
-                notifs[QString::fromStdString(room_id)] = roomNotifs;
-        }
+    for (const auto &room_id : room_ids) {
+        auto roomNotifs                         = getTimelineMentionsForRoom(txn, room_id);
+        notifs[QString::fromStdString(room_id)] = roomNotifs;
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return notifs;
+    return notifs;
 }
 
 std::string
 Cache::previousBatchToken(const std::string &room_id)
 {
-        auto txn     = lmdb::txn::begin(env_, nullptr);
-        auto orderDb = getEventOrderDb(txn, room_id);
+    auto txn     = lmdb::txn::begin(env_, nullptr);
+    auto orderDb = getEventOrderDb(txn, room_id);
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        std::string_view indexVal, val;
-        if (!cursor.get(indexVal, val, MDB_FIRST)) {
-                return "";
-        }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    std::string_view indexVal, val;
+    if (!cursor.get(indexVal, val, MDB_FIRST)) {
+        return "";
+    }
 
-        auto j = json::parse(val);
+    auto j = json::parse(val);
 
-        return j.value("prev_batch", "");
+    return j.value("prev_batch", "");
 }
 
 Cache::Messages
 Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t index, bool forward)
 {
-        // TODO(nico): Limit the messages returned by this maybe?
-        auto orderDb  = getOrderToMessageDb(txn, room_id);
-        auto eventsDb = getEventsDb(txn, room_id);
+    // TODO(nico): Limit the messages returned by this maybe?
+    auto orderDb  = getOrderToMessageDb(txn, room_id);
+    auto eventsDb = getEventsDb(txn, room_id);
 
-        Messages messages{};
+    Messages messages{};
 
-        std::string_view indexVal, event_id;
+    std::string_view indexVal, event_id;
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        if (index == std::numeric_limits::max()) {
-                if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
-                        index = lmdb::from_sv(indexVal);
-                } else {
-                        messages.end_of_cache = true;
-                        return messages;
-                }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    if (index == std::numeric_limits::max()) {
+        if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
+            index = lmdb::from_sv(indexVal);
         } else {
-                if (cursor.get(indexVal, event_id, MDB_SET)) {
-                        index = lmdb::from_sv(indexVal);
-                } else {
-                        messages.end_of_cache = true;
-                        return messages;
-                }
+            messages.end_of_cache = true;
+            return messages;
+        }
+    } else {
+        if (cursor.get(indexVal, event_id, MDB_SET)) {
+            index = lmdb::from_sv(indexVal);
+        } else {
+            messages.end_of_cache = true;
+            return messages;
+        }
+    }
+
+    int counter = 0;
+
+    bool ret;
+    while ((ret = cursor.get(indexVal,
+                             event_id,
+                             counter == 0 ? (forward ? MDB_FIRST : MDB_LAST)
+                                          : (forward ? MDB_NEXT : MDB_PREV))) &&
+           counter++ < BATCH_SIZE) {
+        std::string_view event;
+        bool success = eventsDb.get(txn, event_id, event);
+        if (!success)
+            continue;
+
+        mtx::events::collections::TimelineEvent te;
+        try {
+            mtx::events::collections::from_json(json::parse(event), te);
+        } catch (std::exception &e) {
+            nhlog::db()->error("Failed to parse message from cache {}", e.what());
+            continue;
         }
 
-        int counter = 0;
+        messages.timeline.events.push_back(std::move(te.data));
+    }
+    cursor.close();
 
-        bool ret;
-        while ((ret = cursor.get(indexVal,
-                                 event_id,
-                                 counter == 0 ? (forward ? MDB_FIRST : MDB_LAST)
-                                              : (forward ? MDB_NEXT : MDB_PREV))) &&
-               counter++ < BATCH_SIZE) {
-                std::string_view event;
-                bool success = eventsDb.get(txn, event_id, event);
-                if (!success)
-                        continue;
+    // std::reverse(timeline.events.begin(), timeline.events.end());
+    messages.next_index   = lmdb::from_sv(indexVal);
+    messages.end_of_cache = !ret;
 
-                mtx::events::collections::TimelineEvent te;
-                try {
-                        mtx::events::collections::from_json(json::parse(event), te);
-                } catch (std::exception &e) {
-                        nhlog::db()->error("Failed to parse message from cache {}", e.what());
-                        continue;
-                }
-
-                messages.timeline.events.push_back(std::move(te.data));
-        }
-        cursor.close();
-
-        // std::reverse(timeline.events.begin(), timeline.events.end());
-        messages.next_index   = lmdb::from_sv(indexVal);
-        messages.end_of_cache = !ret;
-
-        return messages;
+    return messages;
 }
 
 std::optional
 Cache::getEvent(const std::string &room_id, const std::string &event_id)
 {
-        auto txn      = ro_txn(env_);
-        auto eventsDb = getEventsDb(txn, room_id);
+    auto txn      = ro_txn(env_);
+    auto eventsDb = getEventsDb(txn, room_id);
 
-        std::string_view event{};
-        bool success = eventsDb.get(txn, event_id, event);
-        if (!success)
-                return {};
+    std::string_view event{};
+    bool success = eventsDb.get(txn, event_id, event);
+    if (!success)
+        return {};
 
-        mtx::events::collections::TimelineEvent te;
-        try {
-                mtx::events::collections::from_json(json::parse(event), te);
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to parse message from cache {}", e.what());
-                return std::nullopt;
-        }
+    mtx::events::collections::TimelineEvent te;
+    try {
+        mtx::events::collections::from_json(json::parse(event), te);
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to parse message from cache {}", e.what());
+        return std::nullopt;
+    }
 
-        return te;
+    return te;
 }
 void
 Cache::storeEvent(const std::string &room_id,
                   const std::string &event_id,
                   const mtx::events::collections::TimelineEvent &event)
 {
-        auto txn        = lmdb::txn::begin(env_);
-        auto eventsDb   = getEventsDb(txn, room_id);
-        auto event_json = mtx::accessors::serialize_event(event.data);
-        eventsDb.put(txn, event_id, event_json.dump());
-        txn.commit();
+    auto txn        = lmdb::txn::begin(env_);
+    auto eventsDb   = getEventsDb(txn, room_id);
+    auto event_json = mtx::accessors::serialize_event(event.data);
+    eventsDb.put(txn, event_id, event_json.dump());
+    txn.commit();
 }
 
 void
@@ -1833,922 +1909,944 @@ Cache::replaceEvent(const std::string &room_id,
                     const std::string &event_id,
                     const mtx::events::collections::TimelineEvent &event)
 {
-        auto txn         = lmdb::txn::begin(env_);
-        auto eventsDb    = getEventsDb(txn, room_id);
-        auto relationsDb = getRelationsDb(txn, room_id);
-        auto event_json  = mtx::accessors::serialize_event(event.data).dump();
+    auto txn         = lmdb::txn::begin(env_);
+    auto eventsDb    = getEventsDb(txn, room_id);
+    auto relationsDb = getRelationsDb(txn, room_id);
+    auto event_json  = mtx::accessors::serialize_event(event.data).dump();
 
-        {
-                eventsDb.del(txn, event_id);
-                eventsDb.put(txn, event_id, event_json);
-                for (auto relation : mtx::accessors::relations(event.data).relations) {
-                        relationsDb.put(txn, relation.event_id, event_id);
-                }
+    {
+        eventsDb.del(txn, event_id);
+        eventsDb.put(txn, event_id, event_json);
+        for (auto relation : mtx::accessors::relations(event.data).relations) {
+            relationsDb.put(txn, relation.event_id, event_id);
         }
+    }
 
-        txn.commit();
+    txn.commit();
 }
 
 std::vector
 Cache::relatedEvents(const std::string &room_id, const std::string &event_id)
 {
-        auto txn         = ro_txn(env_);
-        auto relationsDb = getRelationsDb(txn, room_id);
+    auto txn         = ro_txn(env_);
+    auto relationsDb = getRelationsDb(txn, room_id);
 
-        std::vector related_ids;
+    std::vector related_ids;
 
-        auto related_cursor         = lmdb::cursor::open(txn, relationsDb);
-        std::string_view related_to = event_id, related_event;
-        bool first                  = true;
+    auto related_cursor         = lmdb::cursor::open(txn, relationsDb);
+    std::string_view related_to = event_id, related_event;
+    bool first                  = true;
 
-        try {
-                if (!related_cursor.get(related_to, related_event, MDB_SET))
-                        return {};
+    try {
+        if (!related_cursor.get(related_to, related_event, MDB_SET))
+            return {};
 
-                while (related_cursor.get(
-                  related_to, related_event, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                        first = false;
-                        if (event_id != std::string_view(related_to.data(), related_to.size()))
-                                break;
+        while (
+          related_cursor.get(related_to, related_event, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+            first = false;
+            if (event_id != std::string_view(related_to.data(), related_to.size()))
+                break;
 
-                        related_ids.emplace_back(related_event.data(), related_event.size());
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("related events error: {}", e.what());
+            related_ids.emplace_back(related_event.data(), related_event.size());
         }
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("related events error: {}", e.what());
+    }
 
-        return related_ids;
+    return related_ids;
 }
 
 size_t
 Cache::memberCount(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
-        return getMembersDb(txn, room_id).size(txn);
+    auto txn = ro_txn(env_);
+    return getMembersDb(txn, room_id).size(txn);
 }
 
 QMap
 Cache::roomInfo(bool withInvites)
 {
-        QMap result;
+    QMap result;
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::string_view room_id;
-        std::string_view room_data;
+    std::string_view room_id;
+    std::string_view room_data;
 
-        // Gather info about the joined rooms.
-        auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
-        while (roomsCursor.get(room_id, room_data, MDB_NEXT)) {
-                RoomInfo tmp     = json::parse(std::move(room_data));
-                tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn);
-                result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
+    // Gather info about the joined rooms.
+    auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
+    while (roomsCursor.get(room_id, room_data, MDB_NEXT)) {
+        RoomInfo tmp     = json::parse(std::move(room_data));
+        tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn);
+        result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
+    }
+    roomsCursor.close();
+
+    if (withInvites) {
+        // Gather info about the invites.
+        auto invitesCursor = lmdb::cursor::open(txn, invitesDb_);
+        while (invitesCursor.get(room_id, room_data, MDB_NEXT)) {
+            RoomInfo tmp     = json::parse(room_data);
+            tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
+            result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
         }
-        roomsCursor.close();
+        invitesCursor.close();
+    }
 
-        if (withInvites) {
-                // Gather info about the invites.
-                auto invitesCursor = lmdb::cursor::open(txn, invitesDb_);
-                while (invitesCursor.get(room_id, room_data, MDB_NEXT)) {
-                        RoomInfo tmp     = json::parse(room_data);
-                        tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
-                        result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
-                }
-                invitesCursor.close();
-        }
-
-        return result;
+    return result;
 }
 
 std::string
 Cache::getLastEventId(lmdb::txn &txn, const std::string &room_id)
 {
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getOrderToMessageDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getOrderToMessageDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view indexVal, val;
+    std::string_view indexVal, val;
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        if (!cursor.get(indexVal, val, MDB_LAST)) {
-                return {};
-        }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    if (!cursor.get(indexVal, val, MDB_LAST)) {
+        return {};
+    }
 
-        return std::string(val.data(), val.size());
+    return std::string(val.data(), val.size());
 }
 
 std::optional
 Cache::getTimelineRange(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getOrderToMessageDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    auto txn = ro_txn(env_);
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getOrderToMessageDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view indexVal, val;
+    std::string_view indexVal, val;
 
-        auto cursor = lmdb::cursor::open(txn, orderDb);
-        if (!cursor.get(indexVal, val, MDB_LAST)) {
-                return {};
-        }
+    auto cursor = lmdb::cursor::open(txn, orderDb);
+    if (!cursor.get(indexVal, val, MDB_LAST)) {
+        return {};
+    }
 
-        TimelineRange range{};
-        range.last = lmdb::from_sv(indexVal);
+    TimelineRange range{};
+    range.last = lmdb::from_sv(indexVal);
 
-        if (!cursor.get(indexVal, val, MDB_FIRST)) {
-                return {};
-        }
-        range.first = lmdb::from_sv(indexVal);
+    if (!cursor.get(indexVal, val, MDB_FIRST)) {
+        return {};
+    }
+    range.first = lmdb::from_sv(indexVal);
 
-        return range;
+    return range;
 }
 std::optional
 Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
 {
-        if (event_id.empty() || room_id.empty())
-                return {};
+    if (event_id.empty() || room_id.empty())
+        return {};
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getMessageToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getMessageToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view indexVal{event_id.data(), event_id.size()}, val;
+    std::string_view indexVal{event_id.data(), event_id.size()}, val;
 
-        bool success = orderDb.get(txn, indexVal, val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, indexVal, val);
+    if (!success) {
+        return {};
+    }
 
-        return lmdb::from_sv(val);
+    return lmdb::from_sv(val);
 }
 
 std::optional
 Cache::getEventIndex(const std::string &room_id, std::string_view event_id)
 {
-        if (room_id.empty() || event_id.empty())
-                return {};
+    if (room_id.empty() || event_id.empty())
+        return {};
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getEventToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getEventToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view val;
+    std::string_view val;
 
-        bool success = orderDb.get(txn, event_id, val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, event_id, val);
+    if (!success) {
+        return {};
+    }
 
-        return lmdb::from_sv(val);
+    return lmdb::from_sv(val);
 }
 
 std::optional>
 Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id)
 {
-        if (room_id.empty() || event_id.empty())
-                return {};
+    if (room_id.empty() || event_id.empty())
+        return {};
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        lmdb::dbi orderDb;
-        lmdb::dbi eventOrderDb;
-        lmdb::dbi timelineDb;
-        try {
-                orderDb      = getEventToOrderDb(txn, room_id);
-                eventOrderDb = getEventOrderDb(txn, room_id);
-                timelineDb   = getMessageToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    lmdb::dbi eventOrderDb;
+    lmdb::dbi timelineDb;
+    try {
+        orderDb      = getEventToOrderDb(txn, room_id);
+        eventOrderDb = getEventOrderDb(txn, room_id);
+        timelineDb   = getMessageToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view indexVal;
+    std::string_view indexVal;
 
-        bool success = orderDb.get(txn, event_id, indexVal);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, event_id, indexVal);
+    if (!success) {
+        return {};
+    }
 
-        try {
-                uint64_t prevIdx = lmdb::from_sv(indexVal);
-                std::string prevId{event_id};
-
-                auto cursor = lmdb::cursor::open(txn, eventOrderDb);
-                cursor.get(indexVal, MDB_SET);
-                while (cursor.get(indexVal, event_id, MDB_NEXT)) {
-                        std::string evId = json::parse(event_id)["event_id"].get();
-                        std::string_view temp;
-                        if (timelineDb.get(txn, evId, temp)) {
-                                return std::pair{prevIdx, std::string(prevId)};
-                        } else {
-                                prevIdx = lmdb::from_sv(indexVal);
-                                prevId  = std::move(evId);
-                        }
-                }
+    try {
+        uint64_t prevIdx = lmdb::from_sv(indexVal);
+        std::string prevId{event_id};
 
+        auto cursor = lmdb::cursor::open(txn, eventOrderDb);
+        cursor.get(indexVal, MDB_SET);
+        while (cursor.get(indexVal, event_id, MDB_NEXT)) {
+            std::string evId = json::parse(event_id)["event_id"].get();
+            std::string_view temp;
+            if (timelineDb.get(txn, evId, temp)) {
                 return std::pair{prevIdx, std::string(prevId)};
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error(
-                  "Failed to get last invisible event after {}", event_id, e.what());
-                return {};
+            } else {
+                prevIdx = lmdb::from_sv(indexVal);
+                prevId  = std::move(evId);
+            }
         }
+
+        return std::pair{prevIdx, std::string(prevId)};
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error("Failed to get last invisible event after {}", event_id, e.what());
+        return {};
+    }
 }
 
 std::optional
 Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getEventToOrderDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getEventToOrderDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view val;
+    std::string_view val;
 
-        bool success = orderDb.get(txn, event_id, val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, event_id, val);
+    if (!success) {
+        return {};
+    }
 
-        return lmdb::from_sv(val);
+    return lmdb::from_sv(val);
 }
 
 std::optional
 Cache::getTimelineEventId(const std::string &room_id, uint64_t index)
 {
-        auto txn = ro_txn(env_);
-        lmdb::dbi orderDb;
-        try {
-                orderDb = getOrderToMessageDb(txn, room_id);
-        } catch (lmdb::runtime_error &e) {
-                nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})",
-                                   room_id,
-                                   e.what());
-                return {};
-        }
+    auto txn = ro_txn(env_);
+    lmdb::dbi orderDb;
+    try {
+        orderDb = getOrderToMessageDb(txn, room_id);
+    } catch (lmdb::runtime_error &e) {
+        nhlog::db()->error(
+          "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what());
+        return {};
+    }
 
-        std::string_view val;
+    std::string_view val;
 
-        bool success = orderDb.get(txn, lmdb::to_sv(index), val);
-        if (!success) {
-                return {};
-        }
+    bool success = orderDb.get(txn, lmdb::to_sv(index), val);
+    if (!success) {
+        return {};
+    }
 
-        return std::string(val);
+    return std::string(val);
 }
 
 QHash
 Cache::invites()
 {
-        QHash result;
+    QHash result;
 
-        auto txn    = ro_txn(env_);
-        auto cursor = lmdb::cursor::open(txn, invitesDb_);
+    auto txn    = ro_txn(env_);
+    auto cursor = lmdb::cursor::open(txn, invitesDb_);
 
-        std::string_view room_id, room_data;
+    std::string_view room_id, room_data;
 
-        while (cursor.get(room_id, room_data, MDB_NEXT)) {
-                try {
-                        RoomInfo tmp     = json::parse(room_data);
-                        tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
-                        result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse room info for invite: "
-                                          "room_id ({}), {}: {}",
-                                          room_id,
-                                          std::string(room_data),
-                                          e.what());
-                }
+    while (cursor.get(room_id, room_data, MDB_NEXT)) {
+        try {
+            RoomInfo tmp     = json::parse(room_data);
+            tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
+            result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse room info for invite: "
+                              "room_id ({}), {}: {}",
+                              room_id,
+                              std::string(room_data),
+                              e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        return result;
+    return result;
 }
 
 std::optional
 Cache::invite(std::string_view roomid)
 {
-        std::optional result;
+    std::optional result;
 
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::string_view room_data;
+    std::string_view room_data;
 
-        if (invitesDb_.get(txn, roomid, room_data)) {
-                try {
-                        RoomInfo tmp     = json::parse(room_data);
-                        tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn);
-                        result           = std::move(tmp);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse room info for invite: "
-                                          "room_id ({}), {}: {}",
-                                          roomid,
-                                          std::string(room_data),
-                                          e.what());
-                }
+    if (invitesDb_.get(txn, roomid, room_data)) {
+        try {
+            RoomInfo tmp     = json::parse(room_data);
+            tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn);
+            result           = std::move(tmp);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse room info for invite: "
+                              "room_id ({}), {}: {}",
+                              roomid,
+                              std::string(room_data),
+                              e.what());
         }
+    }
 
-        return result;
+    return result;
 }
 
 QString
 Cache::getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
 
-        if (res) {
-                try {
-                        StateEvent msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    if (res) {
+        try {
+            StateEvent msg = json::parse(std::string_view(event.data(), event.size()));
 
-                        if (!msg.content.url.empty())
-                                return QString::fromStdString(msg.content.url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
-                }
+            if (!msg.content.url.empty())
+                return QString::fromStdString(msg.content.url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
         }
+    }
 
-        // We don't use an avatar for group chats.
-        if (membersdb.size(txn) > 2)
-                return QString();
+    // We don't use an avatar for group chats.
+    if (membersdb.size(txn) > 2)
+        return QString();
 
-        auto cursor = lmdb::cursor::open(txn, membersdb);
-        std::string_view user_id;
-        std::string_view member_data;
-        std::string fallback_url;
+    auto cursor = lmdb::cursor::open(txn, membersdb);
+    std::string_view user_id;
+    std::string_view member_data;
+    std::string fallback_url;
 
-        // Resolve avatar for 1-1 chats.
-        while (cursor.get(user_id, member_data, MDB_NEXT)) {
-                try {
-                        MemberInfo m = json::parse(member_data);
-                        if (user_id == localUserId_.toStdString()) {
-                                fallback_url = m.avatar_url;
-                                continue;
-                        }
+    // Resolve avatar for 1-1 chats.
+    while (cursor.get(user_id, member_data, MDB_NEXT)) {
+        try {
+            MemberInfo m = json::parse(member_data);
+            if (user_id == localUserId_.toStdString()) {
+                fallback_url = m.avatar_url;
+                continue;
+            }
 
-                        cursor.close();
-                        return QString::fromStdString(m.avatar_url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
+            cursor.close();
+            return QString::fromStdString(m.avatar_url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        // Default case when there is only one member.
-        return QString::fromStdString(fallback_url);
+    // Default case when there is only one member.
+    return QString::fromStdString(fallback_url);
 }
 
 QString
 Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
 
-        if (res) {
-                try {
-                        StateEvent msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    if (res) {
+        try {
+            StateEvent msg = json::parse(std::string_view(event.data(), event.size()));
 
-                        if (!msg.content.name.empty())
-                                return QString::fromStdString(msg.content.name);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
-                }
+            if (!msg.content.name.empty())
+                return QString::fromStdString(msg.content.name);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
+        }
+    }
+
+    res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
+
+    if (res) {
+        try {
+            StateEvent msg =
+              json::parse(std::string_view(event.data(), event.size()));
+
+            if (!msg.content.alias.empty())
+                return QString::fromStdString(msg.content.alias);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", e.what());
+        }
+    }
+
+    auto cursor      = lmdb::cursor::open(txn, membersdb);
+    const auto total = membersdb.size(txn);
+
+    std::size_t ii = 0;
+    std::string_view user_id;
+    std::string_view member_data;
+    std::map members;
+
+    while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) {
+        try {
+            members.emplace(user_id, json::parse(member_data));
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
 
-        res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
+        ii++;
+    }
 
-        if (res) {
-                try {
-                        StateEvent msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    cursor.close();
 
-                        if (!msg.content.alias.empty())
-                                return QString::fromStdString(msg.content.alias);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
-                                          e.what());
-                }
+    if (total == 1 && !members.empty())
+        return QString::fromStdString(members.begin()->second.name);
+
+    auto first_member = [&members, this]() {
+        for (const auto &m : members) {
+            if (m.first != localUserId_.toStdString())
+                return QString::fromStdString(m.second.name);
         }
 
-        auto cursor      = lmdb::cursor::open(txn, membersdb);
-        const auto total = membersdb.size(txn);
+        return localUserId_;
+    }();
 
-        std::size_t ii = 0;
-        std::string_view user_id;
-        std::string_view member_data;
-        std::map members;
+    if (total == 2)
+        return first_member;
+    else if (total > 2)
+        return QString("%1 and %2 others").arg(first_member).arg(total - 1);
 
-        while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) {
-                try {
-                        members.emplace(user_id, json::parse(member_data));
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
-
-                ii++;
-        }
-
-        cursor.close();
-
-        if (total == 1 && !members.empty())
-                return QString::fromStdString(members.begin()->second.name);
-
-        auto first_member = [&members, this]() {
-                for (const auto &m : members) {
-                        if (m.first != localUserId_.toStdString())
-                                return QString::fromStdString(m.second.name);
-                }
-
-                return localUserId_;
-        }();
-
-        if (total == 2)
-                return first_member;
-        else if (total > 2)
-                return QString("%1 and %2 others").arg(first_member).arg(total - 1);
-
-        return "Empty Room";
+    return "Empty Room";
 }
 
 mtx::events::state::JoinRule
 Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomJoinRules), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomJoinRules), event);
 
-        if (res) {
-                try {
-                        StateEvent msg = json::parse(event);
-                        return msg.content.join_rule;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StateEvent msg = json::parse(event);
+            return msg.content.join_rule;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what());
         }
-        return state::JoinRule::Knock;
+    }
+    return state::JoinRule::Knock;
 }
 
 bool
 Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomGuestAccess), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomGuestAccess), event);
 
-        if (res) {
-                try {
-                        StateEvent msg = json::parse(event);
-                        return msg.content.guest_access == AccessState::CanJoin;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.guest_access event: {}",
-                                          e.what());
-                }
+    if (res) {
+        try {
+            StateEvent msg = json::parse(event);
+            return msg.content.guest_access == AccessState::CanJoin;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.guest_access event: {}", e.what());
         }
-        return false;
+    }
+    return false;
 }
 
 QString
 Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
 
-        if (res) {
-                try {
-                        StateEvent msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent msg = json::parse(event);
 
-                        if (!msg.content.topic.empty())
-                                return QString::fromStdString(msg.content.topic);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
-                }
+            if (!msg.content.topic.empty())
+                return QString::fromStdString(msg.content.topic);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
         }
+    }
 
-        return QString();
+    return QString();
 }
 
 QString
 Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
 
-        if (res) {
-                try {
-                        StateEvent msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent msg = json::parse(event);
 
-                        if (!msg.content.room_version.empty())
-                                return QString::fromStdString(msg.content.room_version);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
-                }
+            if (!msg.content.room_version.empty())
+                return QString::fromStdString(msg.content.room_version);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
         }
+    }
 
-        nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
-        return QString("1");
+    nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
+    return QString("1");
 }
 
 bool
 Cache::getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
 
-        if (res) {
-                try {
-                        StateEvent msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent msg = json::parse(event);
 
-                        return msg.content.type == mtx::events::state::room_type::space;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
-                }
+            return msg.content.type == mtx::events::state::room_type::space;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
         }
+    }
 
-        nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
-        return false;
+    nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
+    return false;
 }
 
 std::optional
 Cache::getRoomAliases(const std::string &roomid)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        auto txn      = ro_txn(env_);
-        auto statesdb = getStatesDb(txn, roomid);
+    auto txn      = ro_txn(env_);
+    auto statesdb = getStatesDb(txn, roomid);
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event);
 
-        if (res) {
-                try {
-                        StateEvent msg = json::parse(event);
+    if (res) {
+        try {
+            StateEvent msg = json::parse(event);
 
-                        return msg.content;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
-                                          e.what());
-                }
+            return msg.content;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", e.what());
         }
+    }
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 QString
 Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event);
 
-        if (res) {
-                try {
-                        StrippedEvent msg = json::parse(event);
-                        return QString::fromStdString(msg.content.name);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent msg = json::parse(event);
+            return QString::fromStdString(msg.content.name);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.name event: {}", e.what());
         }
+    }
 
-        auto cursor = lmdb::cursor::open(txn, membersdb);
-        std::string_view user_id, member_data;
+    auto cursor = lmdb::cursor::open(txn, membersdb);
+    std::string_view user_id, member_data;
 
-        while (cursor.get(user_id, member_data, MDB_NEXT)) {
-                if (user_id == localUserId_.toStdString())
-                        continue;
+    while (cursor.get(user_id, member_data, MDB_NEXT)) {
+        if (user_id == localUserId_.toStdString())
+            continue;
 
-                try {
-                        MemberInfo tmp = json::parse(member_data);
-                        cursor.close();
+        try {
+            MemberInfo tmp = json::parse(member_data);
+            cursor.close();
 
-                        return QString::fromStdString(tmp.name);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
+            return QString::fromStdString(tmp.name);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        return QString("Empty Room");
+    return QString("Empty Room");
 }
 
 QString
 Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
+    std::string_view event;
+    bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event);
 
-        if (res) {
-                try {
-                        StrippedEvent msg = json::parse(event);
-                        return QString::fromStdString(msg.content.url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent msg = json::parse(event);
+            return QString::fromStdString(msg.content.url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what());
         }
+    }
 
-        auto cursor = lmdb::cursor::open(txn, membersdb);
-        std::string_view user_id, member_data;
+    auto cursor = lmdb::cursor::open(txn, membersdb);
+    std::string_view user_id, member_data;
 
-        while (cursor.get(user_id, member_data, MDB_NEXT)) {
-                if (user_id == localUserId_.toStdString())
-                        continue;
+    while (cursor.get(user_id, member_data, MDB_NEXT)) {
+        if (user_id == localUserId_.toStdString())
+            continue;
 
-                try {
-                        MemberInfo tmp = json::parse(member_data);
-                        cursor.close();
+        try {
+            MemberInfo tmp = json::parse(member_data);
+            cursor.close();
 
-                        return QString::fromStdString(tmp.avatar_url);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse member info: {}", e.what());
-                }
+            return QString::fromStdString(tmp.avatar_url);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse member info: {}", e.what());
         }
+    }
 
-        cursor.close();
+    cursor.close();
 
-        return QString();
+    return QString();
 }
 
 QString
 Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = db.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
+    std::string_view event;
+    bool res = db.get(txn, to_string(mtx::events::EventType::RoomTopic), event);
 
-        if (res) {
-                try {
-                        StrippedEvent msg = json::parse(event);
-                        return QString::fromStdString(msg.content.topic);
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent msg = json::parse(event);
+            return QString::fromStdString(msg.content.topic);
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
         }
+    }
 
-        return QString();
+    return QString();
 }
 
 bool
 Cache::getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        std::string_view event;
-        bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+    std::string_view event;
+    bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
 
-        if (res) {
-                try {
-                        StrippedEvent msg = json::parse(event);
-                        return msg.content.type == mtx::events::state::room_type::space;
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
-                }
+    if (res) {
+        try {
+            StrippedEvent msg = json::parse(event);
+            return msg.content.type == mtx::events::state::room_type::space;
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
         }
+    }
 
-        return false;
+    return false;
 }
 
 std::vector
 Cache::joinedRooms()
 {
-        auto txn         = ro_txn(env_);
-        auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
+    auto txn         = ro_txn(env_);
+    auto roomsCursor = lmdb::cursor::open(txn, roomsDb_);
 
-        std::string_view id, data;
-        std::vector room_ids;
+    std::string_view id, data;
+    std::vector room_ids;
 
-        // Gather the room ids for the joined rooms.
-        while (roomsCursor.get(id, data, MDB_NEXT))
-                room_ids.emplace_back(id);
+    // Gather the room ids for the joined rooms.
+    while (roomsCursor.get(id, data, MDB_NEXT))
+        room_ids.emplace_back(id);
 
-        roomsCursor.close();
+    roomsCursor.close();
 
-        return room_ids;
+    return room_ids;
 }
 
 std::optional
 Cache::getMember(const std::string &room_id, const std::string &user_id)
 {
-        if (user_id.empty() || !env_.handle())
-                return std::nullopt;
-
-        try {
-                auto txn = ro_txn(env_);
-
-                auto membersdb = getMembersDb(txn, room_id);
-
-                std::string_view info;
-                if (membersdb.get(txn, user_id, info)) {
-                        MemberInfo m = json::parse(info);
-                        return m;
-                }
-        } catch (std::exception &e) {
-                nhlog::db()->warn(
-                  "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what());
-        }
+    if (user_id.empty() || !env_.handle())
         return std::nullopt;
+
+    try {
+        auto txn = ro_txn(env_);
+
+        auto membersdb = getMembersDb(txn, room_id);
+
+        std::string_view info;
+        if (membersdb.get(txn, user_id, info)) {
+            MemberInfo m = json::parse(info);
+            return m;
+        }
+    } catch (std::exception &e) {
+        nhlog::db()->warn(
+          "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what());
+    }
+    return std::nullopt;
 }
 
 std::vector
 Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
 {
-        auto txn    = ro_txn(env_);
-        auto db     = getMembersDb(txn, room_id);
-        auto cursor = lmdb::cursor::open(txn, db);
+    auto txn    = ro_txn(env_);
+    auto db     = getMembersDb(txn, room_id);
+    auto cursor = lmdb::cursor::open(txn, db);
 
-        std::size_t currentIndex = 0;
+    std::size_t currentIndex = 0;
 
-        const auto endIndex = std::min(startIndex + len, db.size(txn));
+    const auto endIndex = std::min(startIndex + len, db.size(txn));
 
-        std::vector members;
+    std::vector members;
 
-        std::string_view user_id, user_data;
-        while (cursor.get(user_id, user_data, MDB_NEXT)) {
-                if (currentIndex < startIndex) {
-                        currentIndex += 1;
-                        continue;
-                }
-
-                if (currentIndex >= endIndex)
-                        break;
-
-                try {
-                        MemberInfo tmp = json::parse(user_data);
-                        members.emplace_back(
-                          RoomMember{QString::fromStdString(std::string(user_id)),
-                                     QString::fromStdString(tmp.name)});
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("{}", e.what());
-                }
-
-                currentIndex += 1;
+    std::string_view user_id, user_data;
+    while (cursor.get(user_id, user_data, MDB_NEXT)) {
+        if (currentIndex < startIndex) {
+            currentIndex += 1;
+            continue;
         }
 
-        cursor.close();
+        if (currentIndex >= endIndex)
+            break;
 
-        return members;
+        try {
+            MemberInfo tmp = json::parse(user_data);
+            members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)),
+                                            QString::fromStdString(tmp.name)});
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("{}", e.what());
+        }
+
+        currentIndex += 1;
+    }
+
+    cursor.close();
+
+    return members;
+}
+
+std::vector
+Cache::getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len)
+{
+    auto txn    = ro_txn(env_);
+    auto db     = getInviteMembersDb(txn, room_id);
+    auto cursor = lmdb::cursor::open(txn, db);
+
+    std::size_t currentIndex = 0;
+
+    const auto endIndex = std::min(startIndex + len, db.size(txn));
+
+    std::vector members;
+
+    std::string_view user_id, user_data;
+    while (cursor.get(user_id, user_data, MDB_NEXT)) {
+        if (currentIndex < startIndex) {
+            currentIndex += 1;
+            continue;
+        }
+
+        if (currentIndex >= endIndex)
+            break;
+
+        try {
+            MemberInfo tmp = json::parse(user_data);
+            members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)),
+                                            QString::fromStdString(tmp.name)});
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("{}", e.what());
+        }
+
+        currentIndex += 1;
+    }
+
+    cursor.close();
+
+    return members;
 }
 
 bool
 Cache::isRoomMember(const std::string &user_id, const std::string &room_id)
 {
-        try {
-                auto txn = ro_txn(env_);
-                auto db  = getMembersDb(txn, room_id);
+    try {
+        auto txn = ro_txn(env_);
+        auto db  = getMembersDb(txn, room_id);
 
-                std::string_view value;
-                bool res = db.get(txn, user_id, value);
+        std::string_view value;
+        bool res = db.get(txn, user_id, value);
 
-                return res;
-        } catch (std::exception &e) {
-                nhlog::db()->warn("Failed to read member membership ({}) in room ({}): {}",
-                                  user_id,
-                                  room_id,
-                                  e.what());
-        }
-        return false;
+        return res;
+    } catch (std::exception &e) {
+        nhlog::db()->warn(
+          "Failed to read member membership ({}) in room ({}): {}", user_id, room_id, e.what());
+    }
+    return false;
 }
 
 void
 Cache::savePendingMessage(const std::string &room_id,
                           const mtx::events::collections::TimelineEvent &message)
 {
-        auto txn      = lmdb::txn::begin(env_);
-        auto eventsDb = getEventsDb(txn, room_id);
+    auto txn      = lmdb::txn::begin(env_);
+    auto eventsDb = getEventsDb(txn, room_id);
 
-        mtx::responses::Timeline timeline;
-        timeline.events.push_back(message.data);
-        saveTimelineMessages(txn, eventsDb, room_id, timeline);
+    mtx::responses::Timeline timeline;
+    timeline.events.push_back(message.data);
+    saveTimelineMessages(txn, eventsDb, room_id, timeline);
 
-        auto pending = getPendingMessagesDb(txn, room_id);
+    auto pending = getPendingMessagesDb(txn, room_id);
 
-        int64_t now = QDateTime::currentMSecsSinceEpoch();
-        pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message.data));
+    int64_t now = QDateTime::currentMSecsSinceEpoch();
+    pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message.data));
 
-        txn.commit();
+    txn.commit();
 }
 
 std::optional
 Cache::firstPendingMessage(const std::string &room_id)
 {
-        auto txn     = lmdb::txn::begin(env_);
-        auto pending = getPendingMessagesDb(txn, room_id);
+    auto txn     = lmdb::txn::begin(env_);
+    auto pending = getPendingMessagesDb(txn, room_id);
 
-        {
-                auto pendingCursor = lmdb::cursor::open(txn, pending);
-                std::string_view tsIgnored, pendingTxn;
-                while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
-                        auto eventsDb = getEventsDb(txn, room_id);
-                        std::string_view event;
-                        if (!eventsDb.get(txn, pendingTxn, event)) {
-                                pending.del(txn, tsIgnored, pendingTxn);
-                                continue;
-                        }
+    {
+        auto pendingCursor = lmdb::cursor::open(txn, pending);
+        std::string_view tsIgnored, pendingTxn;
+        while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+            auto eventsDb = getEventsDb(txn, room_id);
+            std::string_view event;
+            if (!eventsDb.get(txn, pendingTxn, event)) {
+                pending.del(txn, tsIgnored, pendingTxn);
+                continue;
+            }
 
-                        try {
-                                mtx::events::collections::TimelineEvent te;
-                                mtx::events::collections::from_json(json::parse(event), te);
+            try {
+                mtx::events::collections::TimelineEvent te;
+                mtx::events::collections::from_json(json::parse(event), te);
 
-                                pendingCursor.close();
-                                txn.commit();
-                                return te;
-                        } catch (std::exception &e) {
-                                nhlog::db()->error("Failed to parse message from cache {}",
-                                                   e.what());
-                                pending.del(txn, tsIgnored, pendingTxn);
-                                continue;
-                        }
-                }
+                pendingCursor.close();
+                txn.commit();
+                return te;
+            } catch (std::exception &e) {
+                nhlog::db()->error("Failed to parse message from cache {}", e.what());
+                pending.del(txn, tsIgnored, pendingTxn);
+                continue;
+            }
         }
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return std::nullopt;
+    return std::nullopt;
 }
 
 void
 Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id)
 {
-        auto txn     = lmdb::txn::begin(env_);
-        auto pending = getPendingMessagesDb(txn, room_id);
+    auto txn     = lmdb::txn::begin(env_);
+    auto pending = getPendingMessagesDb(txn, room_id);
 
-        {
-                auto pendingCursor = lmdb::cursor::open(txn, pending);
-                std::string_view tsIgnored, pendingTxn;
-                while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
-                        if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
-                                lmdb::cursor_del(pendingCursor);
-                }
+    {
+        auto pendingCursor = lmdb::cursor::open(txn, pending);
+        std::string_view tsIgnored, pendingTxn;
+        while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+            if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
+                lmdb::cursor_del(pendingCursor);
         }
+    }
 
-        txn.commit();
+    txn.commit();
 }
 
 void
@@ -2757,390 +2855,398 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
                             const std::string &room_id,
                             const mtx::responses::Timeline &res)
 {
-        if (res.events.empty())
-                return;
+    if (res.events.empty())
+        return;
 
-        auto relationsDb = getRelationsDb(txn, room_id);
+    auto relationsDb = getRelationsDb(txn, room_id);
 
-        auto orderDb     = getEventOrderDb(txn, room_id);
-        auto evToOrderDb = getEventToOrderDb(txn, room_id);
-        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
-        auto order2msgDb = getOrderToMessageDb(txn, room_id);
-        auto pending     = getPendingMessagesDb(txn, room_id);
+    auto orderDb     = getEventOrderDb(txn, room_id);
+    auto evToOrderDb = getEventToOrderDb(txn, room_id);
+    auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+    auto order2msgDb = getOrderToMessageDb(txn, room_id);
+    auto pending     = getPendingMessagesDb(txn, room_id);
 
-        if (res.limited) {
-                lmdb::dbi_drop(txn, orderDb, false);
-                lmdb::dbi_drop(txn, evToOrderDb, false);
-                lmdb::dbi_drop(txn, msg2orderDb, false);
-                lmdb::dbi_drop(txn, order2msgDb, false);
-                lmdb::dbi_drop(txn, pending, true);
+    if (res.limited) {
+        lmdb::dbi_drop(txn, orderDb, false);
+        lmdb::dbi_drop(txn, evToOrderDb, false);
+        lmdb::dbi_drop(txn, msg2orderDb, false);
+        lmdb::dbi_drop(txn, order2msgDb, false);
+        lmdb::dbi_drop(txn, pending, true);
+    }
+
+    using namespace mtx::events;
+    using namespace mtx::events::state;
+
+    std::string_view indexVal, val;
+    uint64_t index = std::numeric_limits::max() / 2;
+    auto cursor    = lmdb::cursor::open(txn, orderDb);
+    if (cursor.get(indexVal, val, MDB_LAST)) {
+        index = lmdb::from_sv(indexVal);
+    }
+
+    uint64_t msgIndex = std::numeric_limits::max() / 2;
+    auto msgCursor    = lmdb::cursor::open(txn, order2msgDb);
+    if (msgCursor.get(indexVal, val, MDB_LAST)) {
+        msgIndex = lmdb::from_sv(indexVal);
+    }
+
+    bool first = true;
+    for (const auto &e : res.events) {
+        auto event  = mtx::accessors::serialize_event(e);
+        auto txn_id = mtx::accessors::transaction_id(e);
+
+        std::string event_id_val = event.value("event_id", "");
+        if (event_id_val.empty()) {
+            nhlog::db()->error("Event without id!");
+            continue;
         }
 
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+        std::string_view event_id = event_id_val;
 
-        std::string_view indexVal, val;
-        uint64_t index = std::numeric_limits::max() / 2;
-        auto cursor    = lmdb::cursor::open(txn, orderDb);
-        if (cursor.get(indexVal, val, MDB_LAST)) {
-                index = lmdb::from_sv(indexVal);
-        }
+        json orderEntry        = json::object();
+        orderEntry["event_id"] = event_id_val;
+        if (first && !res.prev_batch.empty())
+            orderEntry["prev_batch"] = res.prev_batch;
 
-        uint64_t msgIndex = std::numeric_limits::max() / 2;
-        auto msgCursor    = lmdb::cursor::open(txn, order2msgDb);
-        if (msgCursor.get(indexVal, val, MDB_LAST)) {
-                msgIndex = lmdb::from_sv(indexVal);
-        }
+        std::string_view txn_order;
+        if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) {
+            eventsDb.put(txn, event_id, event.dump());
+            eventsDb.del(txn, txn_id);
 
-        bool first = true;
-        for (const auto &e : res.events) {
-                auto event  = mtx::accessors::serialize_event(e);
-                auto txn_id = mtx::accessors::transaction_id(e);
+            std::string_view msg_txn_order;
+            if (msg2orderDb.get(txn, txn_id, msg_txn_order)) {
+                order2msgDb.put(txn, msg_txn_order, event_id);
+                msg2orderDb.put(txn, event_id, msg_txn_order);
+                msg2orderDb.del(txn, txn_id);
+            }
 
-                std::string event_id_val = event.value("event_id", "");
-                if (event_id_val.empty()) {
-                        nhlog::db()->error("Event without id!");
-                        continue;
+            orderDb.put(txn, txn_order, orderEntry.dump());
+            evToOrderDb.put(txn, event_id, txn_order);
+            evToOrderDb.del(txn, txn_id);
+
+            auto relations = mtx::accessors::relations(e);
+            if (!relations.relations.empty()) {
+                for (const auto &r : relations.relations) {
+                    if (!r.event_id.empty()) {
+                        relationsDb.del(txn, r.event_id, txn_id);
+                        relationsDb.put(txn, r.event_id, event_id);
+                    }
                 }
+            }
 
-                std::string_view event_id = event_id_val;
+            auto pendingCursor = lmdb::cursor::open(txn, pending);
+            std::string_view tsIgnored, pendingTxn;
+            while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
+                if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id)
+                    lmdb::cursor_del(pendingCursor);
+            }
+        } else if (auto redaction =
+                     std::get_if>(&e)) {
+            if (redaction->redacts.empty())
+                continue;
 
-                json orderEntry        = json::object();
-                orderEntry["event_id"] = event_id_val;
-                if (first && !res.prev_batch.empty())
-                        orderEntry["prev_batch"] = res.prev_batch;
+            std::string_view oldEvent;
+            bool success = eventsDb.get(txn, redaction->redacts, oldEvent);
+            if (!success)
+                continue;
 
-                std::string_view txn_order;
-                if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) {
-                        eventsDb.put(txn, event_id, event.dump());
-                        eventsDb.del(txn, txn_id);
+            mtx::events::collections::TimelineEvent te;
+            try {
+                mtx::events::collections::from_json(
+                  json::parse(std::string_view(oldEvent.data(), oldEvent.size())), te);
+                // overwrite the content and add redation data
+                std::visit(
+                  [redaction](auto &ev) {
+                      ev.unsigned_data.redacted_because = *redaction;
+                      ev.unsigned_data.redacted_by      = redaction->event_id;
+                  },
+                  te.data);
+                event = mtx::accessors::serialize_event(te.data);
+                event["content"].clear();
 
-                        std::string_view msg_txn_order;
-                        if (msg2orderDb.get(txn, txn_id, msg_txn_order)) {
-                                order2msgDb.put(txn, msg_txn_order, event_id);
-                                msg2orderDb.put(txn, event_id, msg_txn_order);
-                                msg2orderDb.del(txn, txn_id);
-                        }
+            } catch (std::exception &e) {
+                nhlog::db()->error("Failed to parse message from cache {}", e.what());
+                continue;
+            }
 
-                        orderDb.put(txn, txn_order, orderEntry.dump());
-                        evToOrderDb.put(txn, event_id, txn_order);
-                        evToOrderDb.del(txn, txn_id);
+            eventsDb.put(txn, redaction->redacts, event.dump());
+            eventsDb.put(txn, redaction->event_id, json(*redaction).dump());
+        } else {
+            first = false;
 
-                        auto relations = mtx::accessors::relations(e);
-                        if (!relations.relations.empty()) {
-                                for (const auto &r : relations.relations) {
-                                        if (!r.event_id.empty()) {
-                                                relationsDb.del(txn, r.event_id, txn_id);
-                                                relationsDb.put(txn, r.event_id, event_id);
-                                        }
-                                }
-                        }
+            // This check protects against duplicates in the timeline. If the event_id
+            // is already in the DB, we skip putting it (again) in ordered DBs, and only
+            // update the event itself and its relations.
+            std::string_view unused_read;
+            if (!evToOrderDb.get(txn, event_id, unused_read)) {
+                ++index;
 
-                        auto pendingCursor = lmdb::cursor::open(txn, pending);
-                        std::string_view tsIgnored, pendingTxn;
-                        while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) {
-                                if (std::string_view(pendingTxn.data(), pendingTxn.size()) ==
-                                    txn_id)
-                                        lmdb::cursor_del(pendingCursor);
-                        }
-                } else if (auto redaction =
-                             std::get_if>(
-                               &e)) {
-                        if (redaction->redacts.empty())
-                                continue;
+                nhlog::db()->debug("saving '{}'", orderEntry.dump());
 
-                        std::string_view oldEvent;
-                        bool success = eventsDb.get(txn, redaction->redacts, oldEvent);
-                        if (!success)
-                                continue;
+                cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND);
+                evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
 
-                        mtx::events::collections::TimelineEvent te;
-                        try {
-                                mtx::events::collections::from_json(
-                                  json::parse(std::string_view(oldEvent.data(), oldEvent.size())),
-                                  te);
-                                // overwrite the content and add redation data
-                                std::visit(
-                                  [redaction](auto &ev) {
-                                          ev.unsigned_data.redacted_because = *redaction;
-                                          ev.unsigned_data.redacted_by      = redaction->event_id;
-                                  },
-                                  te.data);
-                                event = mtx::accessors::serialize_event(te.data);
-                                event["content"].clear();
+                // TODO(Nico): Allow blacklisting more event types in UI
+                if (!isHiddenEvent(txn, e, room_id)) {
+                    ++msgIndex;
+                    msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND);
 
-                        } catch (std::exception &e) {
-                                nhlog::db()->error("Failed to parse message from cache {}",
-                                                   e.what());
-                                continue;
-                        }
-
-                        eventsDb.put(txn, redaction->redacts, event.dump());
-                        eventsDb.put(txn, redaction->event_id, json(*redaction).dump());
-                } else {
-                        eventsDb.put(txn, event_id, event.dump());
-
-                        ++index;
-
-                        first = false;
-
-                        nhlog::db()->debug("saving '{}'", orderEntry.dump());
-
-                        cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND);
-                        evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
-
-                        // TODO(Nico): Allow blacklisting more event types in UI
-                        if (!isHiddenEvent(txn, e, room_id)) {
-                                ++msgIndex;
-                                msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND);
-
-                                msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
-                        }
-
-                        auto relations = mtx::accessors::relations(e);
-                        if (!relations.relations.empty()) {
-                                for (const auto &r : relations.relations) {
-                                        if (!r.event_id.empty()) {
-                                                relationsDb.put(txn, r.event_id, event_id);
-                                        }
-                                }
-                        }
+                    msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
                 }
+            } else {
+                nhlog::db()->warn("duplicate event '{}'", orderEntry.dump());
+            }
+            eventsDb.put(txn, event_id, event.dump());
+
+            auto relations = mtx::accessors::relations(e);
+            if (!relations.relations.empty()) {
+                for (const auto &r : relations.relations) {
+                    if (!r.event_id.empty()) {
+                        relationsDb.put(txn, r.event_id, event_id);
+                    }
+                }
+            }
         }
+    }
 }
 
 uint64_t
 Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res)
 {
-        auto txn         = lmdb::txn::begin(env_);
-        auto eventsDb    = getEventsDb(txn, room_id);
-        auto relationsDb = getRelationsDb(txn, room_id);
+    auto txn         = lmdb::txn::begin(env_);
+    auto eventsDb    = getEventsDb(txn, room_id);
+    auto relationsDb = getRelationsDb(txn, room_id);
 
-        auto orderDb     = getEventOrderDb(txn, room_id);
-        auto evToOrderDb = getEventToOrderDb(txn, room_id);
-        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
-        auto order2msgDb = getOrderToMessageDb(txn, room_id);
+    auto orderDb     = getEventOrderDb(txn, room_id);
+    auto evToOrderDb = getEventToOrderDb(txn, room_id);
+    auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+    auto order2msgDb = getOrderToMessageDb(txn, room_id);
 
-        std::string_view indexVal, val;
-        uint64_t index = std::numeric_limits::max() / 2;
-        {
-                auto cursor = lmdb::cursor::open(txn, orderDb);
-                if (cursor.get(indexVal, val, MDB_FIRST)) {
-                        index = lmdb::from_sv(indexVal);
-                }
+    std::string_view indexVal, val;
+    uint64_t index = std::numeric_limits::max() / 2;
+    {
+        auto cursor = lmdb::cursor::open(txn, orderDb);
+        if (cursor.get(indexVal, val, MDB_FIRST)) {
+            index = lmdb::from_sv(indexVal);
         }
+    }
 
-        uint64_t msgIndex = std::numeric_limits::max() / 2;
-        {
-                auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
-                if (msgCursor.get(indexVal, val, MDB_FIRST)) {
-                        msgIndex = lmdb::from_sv(indexVal);
-                }
+    uint64_t msgIndex = std::numeric_limits::max() / 2;
+    {
+        auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
+        if (msgCursor.get(indexVal, val, MDB_FIRST)) {
+            msgIndex = lmdb::from_sv(indexVal);
         }
+    }
 
-        if (res.chunk.empty()) {
-                if (orderDb.get(txn, lmdb::to_sv(index), val)) {
-                        auto orderEntry          = json::parse(val);
-                        orderEntry["prev_batch"] = res.end;
-                        orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
-                        txn.commit();
-                }
-                return index;
+    if (res.chunk.empty()) {
+        if (orderDb.get(txn, lmdb::to_sv(index), val)) {
+            auto orderEntry          = json::parse(val);
+            orderEntry["prev_batch"] = res.end;
+            orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
+            txn.commit();
         }
+        return index;
+    }
 
-        std::string event_id_val;
-        for (const auto &e : res.chunk) {
-                if (std::holds_alternative<
-                      mtx::events::RedactionEvent>(e))
-                        continue;
+    std::string event_id_val;
+    for (const auto &e : res.chunk) {
+        if (std::holds_alternative>(e))
+            continue;
 
-                auto event                = mtx::accessors::serialize_event(e);
-                event_id_val              = event["event_id"].get();
-                std::string_view event_id = event_id_val;
-                eventsDb.put(txn, event_id, event.dump());
+        auto event                = mtx::accessors::serialize_event(e);
+        event_id_val              = event["event_id"].get();
+        std::string_view event_id = event_id_val;
 
-                --index;
+        // This check protects against duplicates in the timeline. If the event_id is
+        // already in the DB, we skip putting it (again) in ordered DBs, and only update the
+        // event itself and its relations.
+        std::string_view unused_read;
+        if (!evToOrderDb.get(txn, event_id, unused_read)) {
+            --index;
 
-                json orderEntry        = json::object();
-                orderEntry["event_id"] = event_id_val;
+            json orderEntry        = json::object();
+            orderEntry["event_id"] = event_id_val;
 
-                orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
-                evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
+            orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
+            evToOrderDb.put(txn, event_id, lmdb::to_sv(index));
 
-                // TODO(Nico): Allow blacklisting more event types in UI
-                if (!isHiddenEvent(txn, e, room_id)) {
-                        --msgIndex;
-                        order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id);
+            // TODO(Nico): Allow blacklisting more event types in UI
+            if (!isHiddenEvent(txn, e, room_id)) {
+                --msgIndex;
+                order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id);
 
-                        msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
-                }
-
-                auto relations = mtx::accessors::relations(e);
-                if (!relations.relations.empty()) {
-                        for (const auto &r : relations.relations) {
-                                if (!r.event_id.empty()) {
-                                        relationsDb.put(txn, r.event_id, event_id);
-                                }
-                        }
-                }
+                msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex));
+            }
         }
+        eventsDb.put(txn, event_id, event.dump());
 
-        json orderEntry          = json::object();
-        orderEntry["event_id"]   = event_id_val;
-        orderEntry["prev_batch"] = res.end;
-        orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
+        auto relations = mtx::accessors::relations(e);
+        if (!relations.relations.empty()) {
+            for (const auto &r : relations.relations) {
+                if (!r.event_id.empty()) {
+                    relationsDb.put(txn, r.event_id, event_id);
+                }
+            }
+        }
+    }
 
-        txn.commit();
+    json orderEntry          = json::object();
+    orderEntry["event_id"]   = event_id_val;
+    orderEntry["prev_batch"] = res.end;
+    orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump());
 
-        return msgIndex;
+    txn.commit();
+
+    return msgIndex;
 }
 
 void
 Cache::clearTimeline(const std::string &room_id)
 {
-        auto txn         = lmdb::txn::begin(env_);
-        auto eventsDb    = getEventsDb(txn, room_id);
-        auto relationsDb = getRelationsDb(txn, room_id);
+    auto txn         = lmdb::txn::begin(env_);
+    auto eventsDb    = getEventsDb(txn, room_id);
+    auto relationsDb = getRelationsDb(txn, room_id);
 
-        auto orderDb     = getEventOrderDb(txn, room_id);
-        auto evToOrderDb = getEventToOrderDb(txn, room_id);
-        auto msg2orderDb = getMessageToOrderDb(txn, room_id);
-        auto order2msgDb = getOrderToMessageDb(txn, room_id);
+    auto orderDb     = getEventOrderDb(txn, room_id);
+    auto evToOrderDb = getEventToOrderDb(txn, room_id);
+    auto msg2orderDb = getMessageToOrderDb(txn, room_id);
+    auto order2msgDb = getOrderToMessageDb(txn, room_id);
 
-        std::string_view indexVal, val;
-        auto cursor = lmdb::cursor::open(txn, orderDb);
+    std::string_view indexVal, val;
+    auto cursor = lmdb::cursor::open(txn, orderDb);
 
-        bool start                   = true;
-        bool passed_pagination_token = false;
-        while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
-                start = false;
-                json obj;
+    bool start                   = true;
+    bool passed_pagination_token = false;
+    while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
+        start = false;
+        json obj;
 
-                try {
-                        obj = json::parse(std::string_view(val.data(), val.size()));
-                } catch (std::exception &) {
-                        // workaround bug in the initial db format, where we sometimes didn't store
-                        // json...
-                        obj = {{"event_id", std::string(val.data(), val.size())}};
-                }
-
-                if (passed_pagination_token) {
-                        if (obj.count("event_id") != 0) {
-                                std::string event_id = obj["event_id"].get();
-
-                                if (!event_id.empty()) {
-                                        evToOrderDb.del(txn, event_id);
-                                        eventsDb.del(txn, event_id);
-                                        relationsDb.del(txn, event_id);
-
-                                        std::string_view order{};
-                                        bool exists = msg2orderDb.get(txn, event_id, order);
-                                        if (exists) {
-                                                order2msgDb.del(txn, order);
-                                                msg2orderDb.del(txn, event_id);
-                                        }
-                                }
-                        }
-                        lmdb::cursor_del(cursor);
-                } else {
-                        if (obj.count("prev_batch") != 0)
-                                passed_pagination_token = true;
-                }
+        try {
+            obj = json::parse(std::string_view(val.data(), val.size()));
+        } catch (std::exception &) {
+            // workaround bug in the initial db format, where we sometimes didn't store
+            // json...
+            obj = {{"event_id", std::string(val.data(), val.size())}};
         }
 
-        auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
-        start          = true;
-        while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
-                start = false;
+        if (passed_pagination_token) {
+            if (obj.count("event_id") != 0) {
+                std::string event_id = obj["event_id"].get();
 
-                std::string_view eventId;
-                bool innerStart = true;
-                bool found      = false;
-                while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) {
-                        innerStart = false;
+                if (!event_id.empty()) {
+                    evToOrderDb.del(txn, event_id);
+                    eventsDb.del(txn, event_id);
+                    relationsDb.del(txn, event_id);
 
-                        json obj;
-                        try {
-                                obj = json::parse(std::string_view(eventId.data(), eventId.size()));
-                        } catch (std::exception &) {
-                                obj = {{"event_id", std::string(eventId.data(), eventId.size())}};
-                        }
-
-                        if (obj["event_id"] == std::string(val.data(), val.size())) {
-                                found = true;
-                                break;
-                        }
+                    std::string_view order{};
+                    bool exists = msg2orderDb.get(txn, event_id, order);
+                    if (exists) {
+                        order2msgDb.del(txn, order);
+                        msg2orderDb.del(txn, event_id);
+                    }
                 }
+            }
+            lmdb::cursor_del(cursor);
+        } else {
+            if (obj.count("prev_batch") != 0)
+                passed_pagination_token = true;
+        }
+    }
 
-                if (!found)
-                        break;
+    auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
+    start          = true;
+    while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) {
+        start = false;
+
+        std::string_view eventId;
+        bool innerStart = true;
+        bool found      = false;
+        while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) {
+            innerStart = false;
+
+            json obj;
+            try {
+                obj = json::parse(std::string_view(eventId.data(), eventId.size()));
+            } catch (std::exception &) {
+                obj = {{"event_id", std::string(eventId.data(), eventId.size())}};
+            }
+
+            if (obj["event_id"] == std::string(val.data(), val.size())) {
+                found = true;
+                break;
+            }
         }
 
-        do {
-                lmdb::cursor_del(msgCursor);
-        } while (msgCursor.get(indexVal, val, MDB_PREV));
+        if (!found)
+            break;
+    }
 
-        cursor.close();
-        msgCursor.close();
-        txn.commit();
+    do {
+        lmdb::cursor_del(msgCursor);
+    } while (msgCursor.get(indexVal, val, MDB_PREV));
+
+    cursor.close();
+    msgCursor.close();
+    txn.commit();
 }
 
 mtx::responses::Notifications
 Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id)
 {
-        auto db = getMentionsDb(txn, room_id);
+    auto db = getMentionsDb(txn, room_id);
 
-        if (db.size(txn) == 0) {
-                return mtx::responses::Notifications{};
-        }
+    if (db.size(txn) == 0) {
+        return mtx::responses::Notifications{};
+    }
 
-        mtx::responses::Notifications notif;
-        std::string_view event_id, msg;
+    mtx::responses::Notifications notif;
+    std::string_view event_id, msg;
 
-        auto cursor = lmdb::cursor::open(txn, db);
+    auto cursor = lmdb::cursor::open(txn, db);
 
-        while (cursor.get(event_id, msg, MDB_NEXT)) {
-                auto obj = json::parse(msg);
+    while (cursor.get(event_id, msg, MDB_NEXT)) {
+        auto obj = json::parse(msg);
 
-                if (obj.count("event") == 0)
-                        continue;
+        if (obj.count("event") == 0)
+            continue;
 
-                mtx::responses::Notification notification;
-                mtx::responses::from_json(obj, notification);
+        mtx::responses::Notification notification;
+        mtx::responses::from_json(obj, notification);
 
-                notif.notifications.push_back(notification);
-        }
-        cursor.close();
+        notif.notifications.push_back(notification);
+    }
+    cursor.close();
 
-        std::reverse(notif.notifications.begin(), notif.notifications.end());
+    std::reverse(notif.notifications.begin(), notif.notifications.end());
 
-        return notif;
+    return notif;
 }
 
 //! Add all notifications containing a user mention to the db.
 void
 Cache::saveTimelineMentions(const mtx::responses::Notifications &res)
 {
-        QMap> notifsByRoom;
+    QMap> notifsByRoom;
 
-        // Sort into room-specific 'buckets'
-        for (const auto ¬if : res.notifications) {
-                json val = notif;
-                notifsByRoom[notif.room_id].push_back(notif);
-        }
+    // Sort into room-specific 'buckets'
+    for (const auto ¬if : res.notifications) {
+        json val = notif;
+        notifsByRoom[notif.room_id].push_back(notif);
+    }
 
-        auto txn = lmdb::txn::begin(env_);
-        // Insert the entire set of mentions for each room at a time.
-        QMap>::const_iterator it =
-          notifsByRoom.constBegin();
-        auto end = notifsByRoom.constEnd();
-        while (it != end) {
-                nhlog::db()->debug("Storing notifications for " + it.key());
-                saveTimelineMentions(txn, it.key(), std::move(it.value()));
-                ++it;
-        }
+    auto txn = lmdb::txn::begin(env_);
+    // Insert the entire set of mentions for each room at a time.
+    QMap>::const_iterator it =
+      notifsByRoom.constBegin();
+    auto end = notifsByRoom.constEnd();
+    while (it != end) {
+        nhlog::db()->debug("Storing notifications for " + it.key());
+        saveTimelineMentions(txn, it.key(), std::move(it.value()));
+        ++it;
+    }
 
-        txn.commit();
+    txn.commit();
 }
 
 void
@@ -3148,139 +3254,138 @@ Cache::saveTimelineMentions(lmdb::txn &txn,
                             const std::string &room_id,
                             const QList &res)
 {
-        auto db = getMentionsDb(txn, room_id);
+    auto db = getMentionsDb(txn, room_id);
 
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        for (const auto ¬if : res) {
-                const auto event_id = mtx::accessors::event_id(notif.event);
+    for (const auto ¬if : res) {
+        const auto event_id = mtx::accessors::event_id(notif.event);
 
-                // double check that we have the correct room_id...
-                if (room_id.compare(notif.room_id) != 0) {
-                        return;
-                }
-
-                json obj = notif;
-
-                db.put(txn, event_id, obj.dump());
+        // double check that we have the correct room_id...
+        if (room_id.compare(notif.room_id) != 0) {
+            return;
         }
+
+        json obj = notif;
+
+        db.put(txn, event_id, obj.dump());
+    }
 }
 
 void
 Cache::markSentNotification(const std::string &event_id)
 {
-        auto txn = lmdb::txn::begin(env_);
-        notificationsDb_.put(txn, event_id, "");
-        txn.commit();
+    auto txn = lmdb::txn::begin(env_);
+    notificationsDb_.put(txn, event_id, "");
+    txn.commit();
 }
 
 void
 Cache::removeReadNotification(const std::string &event_id)
 {
-        auto txn = lmdb::txn::begin(env_);
+    auto txn = lmdb::txn::begin(env_);
 
-        notificationsDb_.del(txn, event_id);
+    notificationsDb_.del(txn, event_id);
 
-        txn.commit();
+    txn.commit();
 }
 
 bool
 Cache::isNotificationSent(const std::string &event_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::string_view value;
-        bool res = notificationsDb_.get(txn, event_id, value);
+    std::string_view value;
+    bool res = notificationsDb_.get(txn, event_id, value);
 
-        return res;
+    return res;
 }
 
 std::vector
 Cache::getRoomIds(lmdb::txn &txn)
 {
-        auto db     = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
-        auto cursor = lmdb::cursor::open(txn, db);
+    auto cursor = lmdb::cursor::open(txn, roomsDb_);
 
-        std::vector rooms;
+    std::vector rooms;
 
-        std::string_view room_id, _unused;
-        while (cursor.get(room_id, _unused, MDB_NEXT))
-                rooms.emplace_back(room_id);
+    std::string_view room_id, _unused;
+    while (cursor.get(room_id, _unused, MDB_NEXT))
+        rooms.emplace_back(room_id);
 
-        cursor.close();
+    cursor.close();
 
-        return rooms;
+    return rooms;
 }
 
 void
 Cache::deleteOldMessages()
 {
-        std::string_view indexVal, val;
+    std::string_view indexVal, val;
 
-        auto txn      = lmdb::txn::begin(env_);
-        auto room_ids = getRoomIds(txn);
+    auto txn      = lmdb::txn::begin(env_);
+    auto room_ids = getRoomIds(txn);
 
-        for (const auto &room_id : room_ids) {
-                auto orderDb     = getEventOrderDb(txn, room_id);
-                auto evToOrderDb = getEventToOrderDb(txn, room_id);
-                auto o2m         = getOrderToMessageDb(txn, room_id);
-                auto m2o         = getMessageToOrderDb(txn, room_id);
-                auto eventsDb    = getEventsDb(txn, room_id);
-                auto relationsDb = getRelationsDb(txn, room_id);
-                auto cursor      = lmdb::cursor::open(txn, orderDb);
+    for (const auto &room_id : room_ids) {
+        auto orderDb     = getEventOrderDb(txn, room_id);
+        auto evToOrderDb = getEventToOrderDb(txn, room_id);
+        auto o2m         = getOrderToMessageDb(txn, room_id);
+        auto m2o         = getMessageToOrderDb(txn, room_id);
+        auto eventsDb    = getEventsDb(txn, room_id);
+        auto relationsDb = getRelationsDb(txn, room_id);
+        auto cursor      = lmdb::cursor::open(txn, orderDb);
 
-                uint64_t first, last;
-                if (cursor.get(indexVal, val, MDB_LAST)) {
-                        last = lmdb::from_sv(indexVal);
-                } else {
-                        continue;
-                }
-                if (cursor.get(indexVal, val, MDB_FIRST)) {
-                        first = lmdb::from_sv(indexVal);
-                } else {
-                        continue;
-                }
-
-                size_t message_count = static_cast(last - first);
-                if (message_count < MAX_RESTORED_MESSAGES)
-                        continue;
-
-                bool start = true;
-                while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) &&
-                       message_count-- > MAX_RESTORED_MESSAGES) {
-                        start    = false;
-                        auto obj = json::parse(std::string_view(val.data(), val.size()));
-
-                        if (obj.count("event_id") != 0) {
-                                std::string event_id = obj["event_id"].get();
-                                evToOrderDb.del(txn, event_id);
-                                eventsDb.del(txn, event_id);
-
-                                relationsDb.del(txn, event_id);
-
-                                std::string_view order{};
-                                bool exists = m2o.get(txn, event_id, order);
-                                if (exists) {
-                                        o2m.del(txn, order);
-                                        m2o.del(txn, event_id);
-                                }
-                        }
-                        cursor.del();
-                }
-                cursor.close();
+        uint64_t first, last;
+        if (cursor.get(indexVal, val, MDB_LAST)) {
+            last = lmdb::from_sv(indexVal);
+        } else {
+            continue;
         }
-        txn.commit();
+        if (cursor.get(indexVal, val, MDB_FIRST)) {
+            first = lmdb::from_sv(indexVal);
+        } else {
+            continue;
+        }
+
+        size_t message_count = static_cast(last - first);
+        if (message_count < MAX_RESTORED_MESSAGES)
+            continue;
+
+        bool start = true;
+        while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) &&
+               message_count-- > MAX_RESTORED_MESSAGES) {
+            start    = false;
+            auto obj = json::parse(std::string_view(val.data(), val.size()));
+
+            if (obj.count("event_id") != 0) {
+                std::string event_id = obj["event_id"].get();
+                evToOrderDb.del(txn, event_id);
+                eventsDb.del(txn, event_id);
+
+                relationsDb.del(txn, event_id);
+
+                std::string_view order{};
+                bool exists = m2o.get(txn, event_id, order);
+                if (exists) {
+                    o2m.del(txn, order);
+                    m2o.del(txn, event_id);
+                }
+            }
+            cursor.del();
+        }
+        cursor.close();
+    }
+    txn.commit();
 }
 
 void
 Cache::deleteOldData() noexcept
 {
-        try {
-                deleteOldMessages();
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("failed to delete old messages: {}", e.what());
-        }
+    try {
+        deleteOldMessages();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("failed to delete old messages: {}", e.what());
+    }
 }
 
 void
@@ -3288,241 +3393,245 @@ Cache::updateSpaces(lmdb::txn &txn,
                     const std::set &spaces_with_updates,
                     std::set rooms_with_updates)
 {
-        if (spaces_with_updates.empty() && rooms_with_updates.empty())
-                return;
+    if (spaces_with_updates.empty() && rooms_with_updates.empty())
+        return;
 
-        for (const auto &space : spaces_with_updates) {
-                // delete old entries
-                {
-                        auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
-                        bool first          = true;
-                        std::string_view sp = space, space_child = "";
+    for (const auto &space : spaces_with_updates) {
+        // delete old entries
+        {
+            auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
+            bool first          = true;
+            std::string_view sp = space, space_child = "";
 
-                        if (cursor.get(sp, space_child, MDB_SET)) {
-                                while (cursor.get(
-                                  sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                        first = false;
-                                        spacesParentsDb_.del(txn, space_child, space);
-                                }
-                        }
-                        cursor.close();
-                        spacesChildrenDb_.del(txn, space);
-                }
-
-                for (const auto &event :
-                     getStateEventsWithType(txn, space)) {
-                        if (event.content.via.has_value() && event.state_key.size() > 3 &&
-                            event.state_key.at(0) == '!') {
-                                spacesChildrenDb_.put(txn, space, event.state_key);
-                                spacesParentsDb_.put(txn, event.state_key, space);
-                        }
+            if (cursor.get(sp, space_child, MDB_SET)) {
+                while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                    first = false;
+                    spacesParentsDb_.del(txn, space_child, space);
                 }
+            }
+            cursor.close();
+            spacesChildrenDb_.del(txn, space);
         }
 
-        const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels);
-
-        for (const auto &room : rooms_with_updates) {
-                for (const auto &event :
-                     getStateEventsWithType(txn, room)) {
-                        if (event.content.via.has_value() && event.state_key.size() > 3 &&
-                            event.state_key.at(0) == '!') {
-                                const std::string &space = event.state_key;
-
-                                auto pls =
-                                  getStateEvent(txn, space);
-
-                                if (!pls)
-                                        continue;
-
-                                if (pls->content.user_level(event.sender) >=
-                                    pls->content.state_level(space_event_type)) {
-                                        spacesChildrenDb_.put(txn, space, room);
-                                        spacesParentsDb_.put(txn, room, space);
-                                }
-                        }
-                }
+        for (const auto &event :
+             getStateEventsWithType(txn, space)) {
+            if (event.content.via.has_value() && event.state_key.size() > 3 &&
+                event.state_key.at(0) == '!') {
+                spacesChildrenDb_.put(txn, space, event.state_key);
+                spacesParentsDb_.put(txn, event.state_key, space);
+            }
         }
+
+        for (const auto &r : getRoomIds(txn)) {
+            if (auto parent = getStateEvent(txn, r, space)) {
+                rooms_with_updates.insert(r);
+            }
+        }
+    }
+
+    const auto space_event_type = to_string(mtx::events::EventType::SpaceChild);
+
+    for (const auto &room : rooms_with_updates) {
+        for (const auto &event :
+             getStateEventsWithType(txn, room)) {
+            if (event.content.via.has_value() && event.state_key.size() > 3 &&
+                event.state_key.at(0) == '!') {
+                const std::string &space = event.state_key;
+
+                auto pls = getStateEvent(txn, space);
+
+                if (!pls)
+                    continue;
+
+                if (pls->content.user_level(event.sender) >=
+                    pls->content.state_level(space_event_type)) {
+                    spacesChildrenDb_.put(txn, space, room);
+                    spacesParentsDb_.put(txn, room, space);
+                } else {
+                    nhlog::db()->debug("Skipping {} in {} because of missing PL. {}: {} < {}",
+                                       room,
+                                       space,
+                                       event.sender,
+                                       pls->content.user_level(event.sender),
+                                       pls->content.state_level(space_event_type));
+                }
+            }
+        }
+    }
 }
 
 QMap>
 Cache::spaces()
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        QMap> ret;
-        {
-                auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
-                bool first  = true;
-                std::string_view space_id, space_child;
-                while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) {
-                        first = false;
+    QMap> ret;
+    {
+        auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
+        bool first  = true;
+        std::string_view space_id, space_child;
+        while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) {
+            first = false;
 
-                        if (!space_child.empty()) {
-                                std::string_view room_data;
-                                if (roomsDb_.get(txn, space_id, room_data)) {
-                                        RoomInfo tmp = json::parse(std::move(room_data));
-                                        ret.insert(
-                                          QString::fromUtf8(space_id.data(), space_id.size()), tmp);
-                                } else {
-                                        ret.insert(
-                                          QString::fromUtf8(space_id.data(), space_id.size()),
-                                          std::nullopt);
-                                }
-                        }
+            if (!space_child.empty()) {
+                std::string_view room_data;
+                if (roomsDb_.get(txn, space_id, room_data)) {
+                    RoomInfo tmp = json::parse(std::move(room_data));
+                    ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), tmp);
+                } else {
+                    ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), std::nullopt);
                 }
-                cursor.close();
+            }
         }
+        cursor.close();
+    }
 
-        return ret;
+    return ret;
 }
 
 std::vector
 Cache::getParentRoomIds(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector roomids;
-        {
-                auto cursor         = lmdb::cursor::open(txn, spacesParentsDb_);
-                bool first          = true;
-                std::string_view sp = room_id, space_parent;
-                if (cursor.get(sp, space_parent, MDB_SET)) {
-                        while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                first = false;
+    std::vector roomids;
+    {
+        auto cursor         = lmdb::cursor::open(txn, spacesParentsDb_);
+        bool first          = true;
+        std::string_view sp = room_id, space_parent;
+        if (cursor.get(sp, space_parent, MDB_SET)) {
+            while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                first = false;
 
-                                if (!space_parent.empty())
-                                        roomids.emplace_back(space_parent);
-                        }
-                }
-                cursor.close();
+                if (!space_parent.empty())
+                    roomids.emplace_back(space_parent);
+            }
         }
+        cursor.close();
+    }
 
-        return roomids;
+    return roomids;
 }
 
 std::vector
 Cache::getChildRoomIds(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector roomids;
-        {
-                auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
-                bool first          = true;
-                std::string_view sp = room_id, space_child;
-                if (cursor.get(sp, space_child, MDB_SET)) {
-                        while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                first = false;
+    std::vector roomids;
+    {
+        auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
+        bool first          = true;
+        std::string_view sp = room_id, space_child;
+        if (cursor.get(sp, space_child, MDB_SET)) {
+            while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                first = false;
 
-                                if (!space_child.empty())
-                                        roomids.emplace_back(space_child);
-                        }
-                }
-                cursor.close();
+                if (!space_child.empty())
+                    roomids.emplace_back(space_child);
+            }
         }
+        cursor.close();
+    }
 
-        return roomids;
+    return roomids;
 }
 
 std::vector
 Cache::getImagePacks(const std::string &room_id, std::optional stickers)
 {
-        auto txn = ro_txn(env_);
-        std::vector infos;
+    auto txn = ro_txn(env_);
+    std::vector infos;
 
-        auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack,
-                                          const std::string &source_room,
-                                          const std::string &state_key) {
-                if (!pack.pack || !stickers.has_value() ||
-                    (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
-                        ImagePackInfo info;
-                        info.source_room = source_room;
-                        info.state_key   = state_key;
-                        info.pack.pack   = pack.pack;
+    auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack,
+                                      const std::string &source_room,
+                                      const std::string &state_key) {
+        if (!pack.pack || !stickers.has_value() ||
+            (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
+            ImagePackInfo info;
+            info.source_room = source_room;
+            info.state_key   = state_key;
+            info.pack.pack   = pack.pack;
 
-                        for (const auto &img : pack.images) {
-                                if (stickers.has_value() && img.second.overrides_usage() &&
-                                    (stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
-                                        continue;
+            for (const auto &img : pack.images) {
+                if (stickers.has_value() && img.second.overrides_usage() &&
+                    (stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
+                    continue;
 
-                                info.pack.images.insert(img);
-                        }
+                info.pack.images.insert(img);
+            }
 
-                        if (!info.pack.images.empty())
-                                infos.push_back(std::move(info));
+            if (!info.pack.images.empty())
+                infos.push_back(std::move(info));
+        }
+    };
+
+    // packs from account data
+    if (auto accountpack =
+          getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) {
+        auto tmp =
+          std::get_if>(&*accountpack);
+        if (tmp)
+            addPack(tmp->content, "", "");
+    }
+
+    // packs from rooms, that were enabled globally
+    if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) {
+        auto tmp = std::get_if>(
+          &*roomPacks);
+        if (tmp) {
+            for (const auto &[room_id2, state_to_d] : tmp->content.rooms) {
+                // don't add stickers from this room twice
+                if (room_id2 == room_id)
+                    continue;
+
+                for (const auto &[state_id, d] : state_to_d) {
+                    (void)d;
+                    if (auto pack =
+                          getStateEvent(txn, room_id2, state_id))
+                        addPack(pack->content, room_id2, state_id);
                 }
-        };
-
-        // packs from account data
-        if (auto accountpack =
-              getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) {
-                auto tmp =
-                  std::get_if>(
-                    &*accountpack);
-                if (tmp)
-                        addPack(tmp->content, "", "");
+            }
         }
+    }
 
-        // packs from rooms, that were enabled globally
-        if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) {
-                auto tmp =
-                  std::get_if>(
-                    &*roomPacks);
-                if (tmp) {
-                        for (const auto &[room_id2, state_to_d] : tmp->content.rooms) {
-                                // don't add stickers from this room twice
-                                if (room_id2 == room_id)
-                                        continue;
+    // packs from current room
+    if (auto pack = getStateEvent(txn, room_id)) {
+        addPack(pack->content, room_id, "");
+    }
+    for (const auto &pack : getStateEventsWithType(txn, room_id)) {
+        addPack(pack.content, room_id, pack.state_key);
+    }
 
-                                for (const auto &[state_id, d] : state_to_d) {
-                                        (void)d;
-                                        if (auto pack =
-                                              getStateEvent(
-                                                txn, room_id2, state_id))
-                                                addPack(pack->content, room_id2, state_id);
-                                }
-                        }
-                }
-        }
-
-        // packs from current room
-        if (auto pack = getStateEvent(txn, room_id)) {
-                addPack(pack->content, room_id, "");
-        }
-        for (const auto &pack :
-             getStateEventsWithType(txn, room_id)) {
-                addPack(pack.content, room_id, pack.state_key);
-        }
-
-        return infos;
+    return infos;
 }
 
 std::optional
 Cache::getAccountData(mtx::events::EventType type, const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
-        return getAccountData(txn, type, room_id);
+    auto txn = ro_txn(env_);
+    return getAccountData(txn, type, room_id);
 }
 
 std::optional
 Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
 {
-        try {
-                auto db = getAccountDataDb(txn, room_id);
+    try {
+        auto db = getAccountDataDb(txn, room_id);
 
-                std::string_view data;
-                if (db.get(txn, to_string(type), data)) {
-                        mtx::responses::utils::RoomAccountDataEvents events;
-                        json j = json::array({
-                          json::parse(data),
-                        });
-                        mtx::responses::utils::parse_room_account_data_events(j, events);
-                        if (events.size() == 1)
-                                return events.front();
-                }
-        } catch (...) {
+        std::string_view data;
+        if (db.get(txn, to_string(type), data)) {
+            mtx::responses::utils::RoomAccountDataEvents events;
+            json j = json::array({
+              json::parse(data),
+            });
+            mtx::responses::utils::parse_room_account_data_events(j, events);
+            if (events.size() == 1)
+                return events.front();
         }
-        return std::nullopt;
+    } catch (...) {
+    }
+    return std::nullopt;
 }
 
 bool
@@ -3530,470 +3639,446 @@ Cache::hasEnoughPowerLevel(const std::vector &eventTypes
                            const std::string &room_id,
                            const std::string &user_id)
 {
-        using namespace mtx::events;
-        using namespace mtx::events::state;
+    using namespace mtx::events;
+    using namespace mtx::events::state;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getStatesDb(txn, room_id);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getStatesDb(txn, room_id);
 
-        int64_t min_event_level = std::numeric_limits::max();
-        int64_t user_level      = std::numeric_limits::min();
+    int64_t min_event_level = std::numeric_limits::max();
+    int64_t user_level      = std::numeric_limits::min();
 
-        std::string_view event;
-        bool res = db.get(txn, to_string(EventType::RoomPowerLevels), event);
+    std::string_view event;
+    bool res = db.get(txn, to_string(EventType::RoomPowerLevels), event);
 
-        if (res) {
-                try {
-                        StateEvent msg =
-                          json::parse(std::string_view(event.data(), event.size()));
+    if (res) {
+        try {
+            StateEvent msg = json::parse(std::string_view(event.data(), event.size()));
 
-                        user_level = msg.content.user_level(user_id);
+            user_level = msg.content.user_level(user_id);
 
-                        for (const auto &ty : eventTypes)
-                                min_event_level =
-                                  std::min(min_event_level, msg.content.state_level(to_string(ty)));
-                } catch (const json::exception &e) {
-                        nhlog::db()->warn("failed to parse m.room.power_levels event: {}",
-                                          e.what());
-                }
+            for (const auto &ty : eventTypes)
+                min_event_level = std::min(min_event_level, msg.content.state_level(to_string(ty)));
+        } catch (const json::exception &e) {
+            nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what());
         }
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return user_level >= min_event_level;
+    return user_level >= min_event_level;
 }
 
 std::vector
 Cache::roomMembers(const std::string &room_id)
 {
-        auto txn = ro_txn(env_);
+    auto txn = ro_txn(env_);
 
-        std::vector members;
-        std::string_view user_id, unused;
+    std::vector members;
+    std::string_view user_id, unused;
 
-        auto db = getMembersDb(txn, room_id);
+    auto db = getMembersDb(txn, room_id);
 
-        auto cursor = lmdb::cursor::open(txn, db);
-        while (cursor.get(user_id, unused, MDB_NEXT))
-                members.emplace_back(user_id);
-        cursor.close();
+    auto cursor = lmdb::cursor::open(txn, db);
+    while (cursor.get(user_id, unused, MDB_NEXT))
+        members.emplace_back(user_id);
+    cursor.close();
 
-        return members;
+    return members;
 }
 
 crypto::Trust
 Cache::roomVerificationStatus(const std::string &room_id)
 {
-        crypto::Trust trust = crypto::Verified;
+    crypto::Trust trust = crypto::Verified;
 
-        try {
-                auto txn = lmdb::txn::begin(env_);
+    try {
+        auto txn = lmdb::txn::begin(env_);
 
-                auto db     = getMembersDb(txn, room_id);
-                auto keysDb = getUserKeysDb(txn);
-                std::vector keysToRequest;
+        auto db     = getMembersDb(txn, room_id);
+        auto keysDb = getUserKeysDb(txn);
+        std::vector keysToRequest;
 
-                std::string_view user_id, unused;
-                auto cursor = lmdb::cursor::open(txn, db);
-                while (cursor.get(user_id, unused, MDB_NEXT)) {
-                        auto verif = verificationStatus_(std::string(user_id), txn);
-                        if (verif.unverified_device_count) {
-                                trust = crypto::Unverified;
-                                if (verif.verified_devices.empty() && verif.no_keys) {
-                                        // we probably don't have the keys yet, so query them
-                                        keysToRequest.push_back(std::string(user_id));
-                                }
-                        } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified)
-                                trust = crypto::TOFU;
-                }
-
-                if (!keysToRequest.empty())
-                        markUserKeysOutOfDate(txn, keysDb, keysToRequest, "");
-
-        } catch (std::exception &e) {
-                nhlog::db()->error(
-                  "Failed to calculate verification status for {}: {}", room_id, e.what());
+        std::string_view user_id, unused;
+        auto cursor = lmdb::cursor::open(txn, db);
+        while (cursor.get(user_id, unused, MDB_NEXT)) {
+            auto verif = verificationStatus_(std::string(user_id), txn);
+            if (verif.unverified_device_count) {
                 trust = crypto::Unverified;
+                if (verif.verified_devices.empty() && verif.no_keys) {
+                    // we probably don't have the keys yet, so query them
+                    keysToRequest.push_back(std::string(user_id));
+                }
+            } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified)
+                trust = crypto::TOFU;
         }
 
-        return trust;
+        if (!keysToRequest.empty())
+            markUserKeysOutOfDate(txn, keysDb, keysToRequest, "");
+
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to calculate verification status for {}: {}", room_id, e.what());
+        trust = crypto::Unverified;
+    }
+
+    return trust;
 }
 
 std::map>
 Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
 {
-        std::string_view keys;
+    std::string_view keys;
 
-        try {
-                auto txn = ro_txn(env_);
-                std::map> members;
+    try {
+        auto txn = ro_txn(env_);
+        std::map> members;
 
-                auto db     = getMembersDb(txn, room_id);
-                auto keysDb = getUserKeysDb(txn);
+        auto db     = getMembersDb(txn, room_id);
+        auto keysDb = getUserKeysDb(txn);
 
-                std::string_view user_id, unused;
-                auto cursor = lmdb::cursor::open(txn, db);
-                while (cursor.get(user_id, unused, MDB_NEXT)) {
-                        auto res = keysDb.get(txn, user_id, keys);
+        std::string_view user_id, unused;
+        auto cursor = lmdb::cursor::open(txn, db);
+        while (cursor.get(user_id, unused, MDB_NEXT)) {
+            auto res = keysDb.get(txn, user_id, keys);
 
-                        if (res) {
-                                auto k = json::parse(keys).get();
-                                if (verified_only) {
-                                        auto verif = verificationStatus(std::string(user_id));
-                                        if (verif.user_verified == crypto::Trust::Verified ||
-                                            !verif.verified_devices.empty()) {
-                                                auto keyCopy = k;
-                                                keyCopy.device_keys.clear();
+            if (res) {
+                auto k = json::parse(keys).get();
+                if (verified_only) {
+                    auto verif = verificationStatus_(std::string(user_id), txn);
 
-                                                std::copy_if(
-                                                  k.device_keys.begin(),
-                                                  k.device_keys.end(),
-                                                  std::inserter(keyCopy.device_keys,
-                                                                keyCopy.device_keys.end()),
-                                                  [&verif](const auto &key) {
-                                                          auto curve25519 = key.second.keys.find(
-                                                            "curve25519:" + key.first);
-                                                          if (curve25519 == key.second.keys.end())
-                                                                  return false;
-                                                          if (auto t =
-                                                                verif.verified_device_keys.find(
-                                                                  curve25519->second);
-                                                              t ==
-                                                                verif.verified_device_keys.end() ||
-                                                              t->second != crypto::Trust::Verified)
-                                                                  return false;
+                    if (verif.user_verified == crypto::Trust::Verified ||
+                        !verif.verified_devices.empty()) {
+                        auto keyCopy = k;
+                        keyCopy.device_keys.clear();
 
-                                                          return key.first ==
-                                                                   key.second.device_id &&
-                                                                 std::find(
-                                                                   verif.verified_devices.begin(),
-                                                                   verif.verified_devices.end(),
-                                                                   key.first) !=
-                                                                   verif.verified_devices.end();
-                                                  });
+                        std::copy_if(
+                          k.device_keys.begin(),
+                          k.device_keys.end(),
+                          std::inserter(keyCopy.device_keys, keyCopy.device_keys.end()),
+                          [&verif](const auto &key) {
+                              auto curve25519 = key.second.keys.find("curve25519:" + key.first);
+                              if (curve25519 == key.second.keys.end())
+                                  return false;
+                              if (auto t = verif.verified_device_keys.find(curve25519->second);
+                                  t == verif.verified_device_keys.end() ||
+                                  t->second != crypto::Trust::Verified)
+                                  return false;
 
-                                                if (!keyCopy.device_keys.empty())
-                                                        members[std::string(user_id)] =
-                                                          std::move(keyCopy);
-                                        }
-                                } else {
-                                        members[std::string(user_id)] = std::move(k);
-                                }
-                        } else {
-                                if (!verified_only)
-                                        members[std::string(user_id)] = {};
-                        }
+                              return key.first == key.second.device_id &&
+                                     std::find(verif.verified_devices.begin(),
+                                               verif.verified_devices.end(),
+                                               key.first) != verif.verified_devices.end();
+                          });
+
+                        if (!keyCopy.device_keys.empty())
+                            members[std::string(user_id)] = std::move(keyCopy);
+                    }
+                } else {
+                    members[std::string(user_id)] = std::move(k);
                 }
-                cursor.close();
-
-                return members;
-        } catch (std::exception &) {
-                return {};
+            } else {
+                if (!verified_only)
+                    members[std::string(user_id)] = {};
+            }
         }
+        cursor.close();
+
+        return members;
+    } catch (std::exception &e) {
+        nhlog::db()->debug("Error retrieving members: {}", e.what());
+        return {};
+    }
 }
 
 QString
 Cache::displayName(const QString &room_id, const QString &user_id)
 {
-        if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
-            info && !info->name.empty())
-                return QString::fromStdString(info->name);
+    if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
+        info && !info->name.empty())
+        return QString::fromStdString(info->name);
 
-        return user_id;
+    return user_id;
 }
 
 std::string
 Cache::displayName(const std::string &room_id, const std::string &user_id)
 {
-        if (auto info = getMember(room_id, user_id); info && !info->name.empty())
-                return info->name;
+    if (auto info = getMember(room_id, user_id); info && !info->name.empty())
+        return info->name;
 
-        return user_id;
+    return user_id;
 }
 
 QString
 Cache::avatarUrl(const QString &room_id, const QString &user_id)
 {
-        if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
-            info && !info->avatar_url.empty())
-                return QString::fromStdString(info->avatar_url);
+    if (auto info = getMember(room_id.toStdString(), user_id.toStdString());
+        info && !info->avatar_url.empty())
+        return QString::fromStdString(info->avatar_url);
 
-        return "";
+    return "";
 }
 
 mtx::presence::PresenceState
 Cache::presenceState(const std::string &user_id)
 {
-        if (user_id.empty())
-                return {};
+    if (user_id.empty())
+        return {};
 
-        std::string_view presenceVal;
+    std::string_view presenceVal;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getPresenceDb(txn);
-        auto res = db.get(txn, user_id, presenceVal);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getPresenceDb(txn);
+    auto res = db.get(txn, user_id, presenceVal);
 
-        mtx::presence::PresenceState state = mtx::presence::offline;
+    mtx::presence::PresenceState state = mtx::presence::offline;
 
-        if (res) {
-                mtx::events::presence::Presence presence =
-                  json::parse(std::string_view(presenceVal.data(), presenceVal.size()));
-                state = presence.presence;
-        }
+    if (res) {
+        mtx::events::presence::Presence presence =
+          json::parse(std::string_view(presenceVal.data(), presenceVal.size()));
+        state = presence.presence;
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return state;
+    return state;
 }
 
 std::string
 Cache::statusMessage(const std::string &user_id)
 {
-        if (user_id.empty())
-                return {};
+    if (user_id.empty())
+        return {};
 
-        std::string_view presenceVal;
+    std::string_view presenceVal;
 
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getPresenceDb(txn);
-        auto res = db.get(txn, user_id, presenceVal);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getPresenceDb(txn);
+    auto res = db.get(txn, user_id, presenceVal);
 
-        std::string status_msg;
+    std::string status_msg;
 
-        if (res) {
-                mtx::events::presence::Presence presence = json::parse(presenceVal);
-                status_msg                               = presence.status_msg;
-        }
+    if (res) {
+        mtx::events::presence::Presence presence = json::parse(presenceVal);
+        status_msg                               = presence.status_msg;
+    }
 
-        txn.commit();
+    txn.commit();
 
-        return status_msg;
+    return status_msg;
 }
 
 void
 to_json(json &j, const UserKeyCache &info)
 {
-        j["device_keys"]        = info.device_keys;
-        j["seen_device_keys"]   = info.seen_device_keys;
-        j["seen_device_ids"]    = info.seen_device_ids;
-        j["master_keys"]        = info.master_keys;
-        j["master_key_changed"] = info.master_key_changed;
-        j["user_signing_keys"]  = info.user_signing_keys;
-        j["self_signing_keys"]  = info.self_signing_keys;
-        j["updated_at"]         = info.updated_at;
-        j["last_changed"]       = info.last_changed;
+    j["device_keys"]        = info.device_keys;
+    j["seen_device_keys"]   = info.seen_device_keys;
+    j["seen_device_ids"]    = info.seen_device_ids;
+    j["master_keys"]        = info.master_keys;
+    j["master_key_changed"] = info.master_key_changed;
+    j["user_signing_keys"]  = info.user_signing_keys;
+    j["self_signing_keys"]  = info.self_signing_keys;
+    j["updated_at"]         = info.updated_at;
+    j["last_changed"]       = info.last_changed;
 }
 
 void
 from_json(const json &j, UserKeyCache &info)
 {
-        info.device_keys = j.value("device_keys", std::map{});
-        info.seen_device_keys   = j.value("seen_device_keys", std::set{});
-        info.seen_device_ids    = j.value("seen_device_ids", std::set{});
-        info.master_keys        = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
-        info.master_key_changed = j.value("master_key_changed", false);
-        info.user_signing_keys  = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
-        info.self_signing_keys  = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{});
-        info.updated_at         = j.value("updated_at", "");
-        info.last_changed       = j.value("last_changed", "");
+    info.device_keys = j.value("device_keys", std::map{});
+    info.seen_device_keys   = j.value("seen_device_keys", std::set{});
+    info.seen_device_ids    = j.value("seen_device_ids", std::set{});
+    info.master_keys        = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
+    info.master_key_changed = j.value("master_key_changed", false);
+    info.user_signing_keys  = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
+    info.self_signing_keys  = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{});
+    info.updated_at         = j.value("updated_at", "");
+    info.last_changed       = j.value("last_changed", "");
 }
 
 std::optional
 Cache::userKeys(const std::string &user_id)
 {
-        auto txn = ro_txn(env_);
-        return userKeys_(user_id, txn);
+    auto txn = ro_txn(env_);
+    return userKeys_(user_id, txn);
 }
 
 std::optional
 Cache::userKeys_(const std::string &user_id, lmdb::txn &txn)
 {
-        std::string_view keys;
+    std::string_view keys;
 
-        try {
-                auto db  = getUserKeysDb(txn);
-                auto res = db.get(txn, user_id, keys);
+    try {
+        auto db  = getUserKeysDb(txn);
+        auto res = db.get(txn, user_id, keys);
 
-                if (res) {
-                        return json::parse(keys).get();
-                } else {
-                        return {};
-                }
-        } catch (std::exception &e) {
-                nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
-                return {};
+        if (res) {
+            return json::parse(keys).get();
+        } else {
+            return {};
         }
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
+        return {};
+    }
 }
 
 void
 Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
 {
-        auto txn = lmdb::txn::begin(env_);
-        auto db  = getUserKeysDb(txn);
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getUserKeysDb(txn);
 
-        std::map updates;
+    std::map updates;
 
-        for (const auto &[user, keys] : keyQuery.device_keys)
-                updates[user].device_keys = keys;
-        for (const auto &[user, keys] : keyQuery.master_keys)
-                updates[user].master_keys = keys;
-        for (const auto &[user, keys] : keyQuery.user_signing_keys)
-                updates[user].user_signing_keys = keys;
-        for (const auto &[user, keys] : keyQuery.self_signing_keys)
-                updates[user].self_signing_keys = keys;
+    for (const auto &[user, keys] : keyQuery.device_keys)
+        updates[user].device_keys = keys;
+    for (const auto &[user, keys] : keyQuery.master_keys)
+        updates[user].master_keys = keys;
+    for (const auto &[user, keys] : keyQuery.user_signing_keys)
+        updates[user].user_signing_keys = keys;
+    for (const auto &[user, keys] : keyQuery.self_signing_keys)
+        updates[user].self_signing_keys = keys;
 
-        for (auto &[user, update] : updates) {
-                nhlog::db()->debug("Updated user keys: {}", user);
+    for (auto &[user, update] : updates) {
+        nhlog::db()->debug("Updated user keys: {}", user);
 
-                auto updateToWrite = update;
+        auto updateToWrite = update;
 
-                std::string_view oldKeys;
-                auto res = db.get(txn, user, oldKeys);
+        std::string_view oldKeys;
+        auto res = db.get(txn, user, oldKeys);
 
-                if (res) {
-                        updateToWrite     = json::parse(oldKeys).get();
-                        auto last_changed = updateToWrite.last_changed;
-                        // skip if we are tracking this and expect it to be up to date with the last
-                        // sync token
-                        if (!last_changed.empty() && last_changed != sync_token) {
-                                nhlog::db()->debug("Not storing update for user {}, because "
-                                                   "last_changed {}, but we fetched update for {}",
-                                                   user,
-                                                   last_changed,
-                                                   sync_token);
-                                continue;
+        if (res) {
+            updateToWrite     = json::parse(oldKeys).get();
+            auto last_changed = updateToWrite.last_changed;
+            // skip if we are tracking this and expect it to be up to date with the last
+            // sync token
+            if (!last_changed.empty() && last_changed != sync_token) {
+                nhlog::db()->debug("Not storing update for user {}, because "
+                                   "last_changed {}, but we fetched update for {}",
+                                   user,
+                                   last_changed,
+                                   sync_token);
+                continue;
+            }
+
+            if (!updateToWrite.master_keys.keys.empty() &&
+                update.master_keys.keys != updateToWrite.master_keys.keys) {
+                nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}",
+                                   user,
+                                   updateToWrite.master_keys.keys.size(),
+                                   update.master_keys.keys.size());
+                updateToWrite.master_key_changed = true;
+            }
+
+            updateToWrite.master_keys       = update.master_keys;
+            updateToWrite.self_signing_keys = update.self_signing_keys;
+            updateToWrite.user_signing_keys = update.user_signing_keys;
+
+            auto oldDeviceKeys = std::move(updateToWrite.device_keys);
+            updateToWrite.device_keys.clear();
+
+            // Don't insert keys, which we have seen once already
+            for (const auto &[device_id, device_keys] : update.device_keys) {
+                if (oldDeviceKeys.count(device_id) &&
+                    oldDeviceKeys.at(device_id).keys == device_keys.keys) {
+                    // this is safe, since the keys are the same
+                    updateToWrite.device_keys[device_id] = device_keys;
+                } else {
+                    bool keyReused = false;
+                    for (const auto &[key_id, key] : device_keys.keys) {
+                        (void)key_id;
+                        if (updateToWrite.seen_device_keys.count(key)) {
+                            nhlog::crypto()->warn(
+                              "Key '{}' reused by ({}: {})", key, user, device_id);
+                            keyReused = true;
+                            break;
+                        }
+                        if (updateToWrite.seen_device_ids.count(device_id)) {
+                            nhlog::crypto()->warn("device_id '{}' reused by ({})", device_id, user);
+                            keyReused = true;
+                            break;
+                        }
+                    }
+
+                    if (!keyReused && !oldDeviceKeys.count(device_id)) {
+                        // ensure the key has a valid signature from itself
+                        std::string device_signing_key = "ed25519:" + device_keys.device_id;
+                        if (device_id != device_keys.device_id) {
+                            nhlog::crypto()->warn("device {}:{} has a different device id "
+                                                  "in the body: {}",
+                                                  user,
+                                                  device_id,
+                                                  device_keys.device_id);
+                            continue;
+                        }
+                        if (!device_keys.signatures.count(user) ||
+                            !device_keys.signatures.at(user).count(device_signing_key)) {
+                            nhlog::crypto()->warn("device {}:{} has no signature", user, device_id);
+                            continue;
                         }
 
-                        if (!updateToWrite.master_keys.keys.empty() &&
-                            update.master_keys.keys != updateToWrite.master_keys.keys) {
-                                nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}",
-                                                   user,
-                                                   updateToWrite.master_keys.keys.size(),
-                                                   update.master_keys.keys.size());
-                                updateToWrite.master_key_changed = true;
+                        if (!mtx::crypto::ed25519_verify_signature(
+                              device_keys.keys.at(device_signing_key),
+                              json(device_keys),
+                              device_keys.signatures.at(user).at(device_signing_key))) {
+                            nhlog::crypto()->warn(
+                              "device {}:{} has an invalid signature", user, device_id);
+                            continue;
                         }
 
-                        updateToWrite.master_keys       = update.master_keys;
-                        updateToWrite.self_signing_keys = update.self_signing_keys;
-                        updateToWrite.user_signing_keys = update.user_signing_keys;
-
-                        auto oldDeviceKeys = std::move(updateToWrite.device_keys);
-                        updateToWrite.device_keys.clear();
-
-                        // Don't insert keys, which we have seen once already
-                        for (const auto &[device_id, device_keys] : update.device_keys) {
-                                if (oldDeviceKeys.count(device_id) &&
-                                    oldDeviceKeys.at(device_id).keys == device_keys.keys) {
-                                        // this is safe, since the keys are the same
-                                        updateToWrite.device_keys[device_id] = device_keys;
-                                } else {
-                                        bool keyReused = false;
-                                        for (const auto &[key_id, key] : device_keys.keys) {
-                                                (void)key_id;
-                                                if (updateToWrite.seen_device_keys.count(key)) {
-                                                        nhlog::crypto()->warn(
-                                                          "Key '{}' reused by ({}: {})",
-                                                          key,
-                                                          user,
-                                                          device_id);
-                                                        keyReused = true;
-                                                        break;
-                                                }
-                                                if (updateToWrite.seen_device_ids.count(
-                                                      device_id)) {
-                                                        nhlog::crypto()->warn(
-                                                          "device_id '{}' reused by ({})",
-                                                          device_id,
-                                                          user);
-                                                        keyReused = true;
-                                                        break;
-                                                }
-                                        }
-
-                                        if (!keyReused && !oldDeviceKeys.count(device_id)) {
-                                                // ensure the key has a valid signature from itself
-                                                std::string device_signing_key =
-                                                  "ed25519:" + device_keys.device_id;
-                                                if (device_id != device_keys.device_id) {
-                                                        nhlog::crypto()->warn(
-                                                          "device {}:{} has a different device id "
-                                                          "in the body: {}",
-                                                          user,
-                                                          device_id,
-                                                          device_keys.device_id);
-                                                        continue;
-                                                }
-                                                if (!device_keys.signatures.count(user) ||
-                                                    !device_keys.signatures.at(user).count(
-                                                      device_signing_key)) {
-                                                        nhlog::crypto()->warn(
-                                                          "device {}:{} has no signature",
-                                                          user,
-                                                          device_id);
-                                                        continue;
-                                                }
-
-                                                if (!mtx::crypto::ed25519_verify_signature(
-                                                      device_keys.keys.at(device_signing_key),
-                                                      json(device_keys),
-                                                      device_keys.signatures.at(user).at(
-                                                        device_signing_key))) {
-                                                        nhlog::crypto()->warn(
-                                                          "device {}:{} has an invalid signature",
-                                                          user,
-                                                          device_id);
-                                                        continue;
-                                                }
-
-                                                updateToWrite.device_keys[device_id] = device_keys;
-                                        }
-                                }
-
-                                for (const auto &[key_id, key] : device_keys.keys) {
-                                        (void)key_id;
-                                        updateToWrite.seen_device_keys.insert(key);
-                                }
-                                updateToWrite.seen_device_ids.insert(device_id);
-                        }
+                        updateToWrite.device_keys[device_id] = device_keys;
+                    }
                 }
-                updateToWrite.updated_at = sync_token;
-                db.put(txn, user, json(updateToWrite).dump());
-        }
 
-        txn.commit();
-
-        std::map tmp;
-        const auto local_user = utils::localUser().toStdString();
-
-        {
-                std::unique_lock lock(verification_storage.verification_storage_mtx);
-                for (auto &[user_id, update] : updates) {
-                        (void)update;
-                        if (user_id == local_user) {
-                                std::swap(tmp, verification_storage.status);
-                        } else {
-                                verification_storage.status.erase(user_id);
-                        }
+                for (const auto &[key_id, key] : device_keys.keys) {
+                    (void)key_id;
+                    updateToWrite.seen_device_keys.insert(key);
                 }
+                updateToWrite.seen_device_ids.insert(device_id);
+            }
         }
+        updateToWrite.updated_at = sync_token;
+        db.put(txn, user, json(updateToWrite).dump());
+    }
 
+    txn.commit();
+
+    std::map tmp;
+    const auto local_user = utils::localUser().toStdString();
+
+    {
+        std::unique_lock lock(verification_storage.verification_storage_mtx);
         for (auto &[user_id, update] : updates) {
-                (void)update;
-                if (user_id == local_user) {
-                        for (const auto &[user, status] : tmp) {
-                                (void)status;
-                                emit verificationStatusChanged(user);
-                        }
-                }
-                emit verificationStatusChanged(user_id);
+            (void)update;
+            if (user_id == local_user) {
+                std::swap(tmp, verification_storage.status);
+            } else {
+                verification_storage.status.erase(user_id);
+            }
         }
+    }
+
+    for (auto &[user_id, update] : updates) {
+        (void)update;
+        if (user_id == local_user) {
+            for (const auto &[user, status] : tmp) {
+                (void)status;
+                emit verificationStatusChanged(user);
+            }
+        }
+        emit verificationStatusChanged(user_id);
+    }
 }
 
 void
-Cache::deleteUserKeys(lmdb::txn &txn, lmdb::dbi &db, const std::vector &user_ids)
+Cache::markUserKeysOutOfDate(const std::vector &user_ids)
 {
-        for (const auto &user_id : user_ids)
-                db.del(txn, user_id);
+    auto currentBatchToken = nextBatchToken();
+    auto txn               = lmdb::txn::begin(env_);
+    auto db                = getUserKeysDb(txn);
+    markUserKeysOutOfDate(txn, db, user_ids, currentBatchToken);
+    txn.commit();
 }
 
 void
@@ -4002,752 +4087,771 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn,
                              const std::vector &user_ids,
                              const std::string &sync_token)
 {
-        mtx::requests::QueryKeys query;
-        query.token = sync_token;
+    mtx::requests::QueryKeys query;
+    query.token = sync_token;
 
-        for (const auto &user : user_ids) {
-                nhlog::db()->debug("Marking user keys out of date: {}", user);
+    for (const auto &user : user_ids) {
+        nhlog::db()->debug("Marking user keys out of date: {}", user);
 
-                std::string_view oldKeys;
+        std::string_view oldKeys;
 
-                UserKeyCache cacheEntry;
-                auto res = db.get(txn, user, oldKeys);
-                if (res) {
-                        cacheEntry = json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
-                                       .get();
-                }
-                cacheEntry.last_changed = sync_token;
-
-                db.put(txn, user, json(cacheEntry).dump());
-
-                query.device_keys[user] = {};
+        UserKeyCache cacheEntry;
+        auto res = db.get(txn, user, oldKeys);
+        if (res) {
+            cacheEntry =
+              json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get();
         }
+        cacheEntry.last_changed = sync_token;
 
-        if (!query.device_keys.empty())
-                http::client()->query_keys(query,
-                                           [this, sync_token](const mtx::responses::QueryKeys &keys,
-                                                              mtx::http::RequestErr err) {
-                                                   if (err) {
-                                                           nhlog::net()->warn(
-                                                             "failed to query device keys: {} {}",
-                                                             err->matrix_error.error,
-                                                             static_cast(err->status_code));
-                                                           return;
-                                                   }
+        db.put(txn, user, json(cacheEntry).dump());
 
-                                                   emit userKeysUpdate(sync_token, keys);
-                                           });
+        query.device_keys[user] = {};
+    }
+
+    if (!query.device_keys.empty())
+        http::client()->query_keys(
+          query,
+          [this, sync_token](const mtx::responses::QueryKeys &keys, mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::net()->warn("failed to query device keys: {} {}",
+                                     err->matrix_error.error,
+                                     static_cast(err->status_code));
+                  return;
+              }
+
+              emit userKeysUpdate(sync_token, keys);
+          });
 }
 
 void
 Cache::query_keys(const std::string &user_id,
                   std::function cb)
 {
-        mtx::requests::QueryKeys req;
-        std::string last_changed;
-        {
-                auto txn    = ro_txn(env_);
-                auto cache_ = userKeys_(user_id, txn);
+    mtx::requests::QueryKeys req;
+    std::string last_changed;
+    {
+        auto txn    = ro_txn(env_);
+        auto cache_ = userKeys_(user_id, txn);
 
-                if (cache_.has_value()) {
-                        if (cache_->updated_at == cache_->last_changed) {
-                                cb(cache_.value(), {});
-                                return;
-                        } else
-                                nhlog::db()->info("Keys outdated for {}: {} vs {}",
-                                                  user_id,
-                                                  cache_->updated_at,
-                                                  cache_->last_changed);
-                } else
-                        nhlog::db()->info("No keys found for {}", user_id);
+        if (cache_.has_value()) {
+            if (cache_->updated_at == cache_->last_changed) {
+                cb(cache_.value(), {});
+                return;
+            } else
+                nhlog::db()->info("Keys outdated for {}: {} vs {}",
+                                  user_id,
+                                  cache_->updated_at,
+                                  cache_->last_changed);
+        } else
+            nhlog::db()->info("No keys found for {}", user_id);
 
-                req.device_keys[user_id] = {};
+        req.device_keys[user_id] = {};
 
-                if (cache_)
-                        last_changed = cache_->last_changed;
-                req.token = last_changed;
-        }
+        if (cache_)
+            last_changed = cache_->last_changed;
+        req.token = last_changed;
+    }
 
-        // use context object so that we can disconnect again
-        QObject *context{new QObject(this)};
-        QObject::connect(
-          this,
-          &Cache::verificationStatusChanged,
-          context,
-          [cb, user_id, context_ = context, this](std::string updated_user) mutable {
-                  if (user_id == updated_user) {
-                          context_->deleteLater();
-                          auto txn  = ro_txn(env_);
-                          auto keys = this->userKeys_(user_id, txn);
-                          cb(keys.value_or(UserKeyCache{}), {});
-                  }
-          },
-          Qt::QueuedConnection);
+    // use context object so that we can disconnect again
+    QObject *context{new QObject(this)};
+    QObject::connect(
+      this,
+      &Cache::verificationStatusChanged,
+      context,
+      [cb, user_id, context_ = context, this](std::string updated_user) mutable {
+          if (user_id == updated_user) {
+              context_->deleteLater();
+              auto txn  = ro_txn(env_);
+              auto keys = this->userKeys_(user_id, txn);
+              cb(keys.value_or(UserKeyCache{}), {});
+          }
+      },
+      Qt::QueuedConnection);
 
-        http::client()->query_keys(
-          req,
-          [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res,
-                                            mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to query device keys: {},{}",
-                                             mtx::errors::to_string(err->matrix_error.errcode),
-                                             static_cast(err->status_code));
-                          cb({}, err);
-                          return;
-                  }
+    http::client()->query_keys(
+      req,
+      [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res,
+                                        mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to query device keys: {},{}",
+                                 mtx::errors::to_string(err->matrix_error.errcode),
+                                 static_cast(err->status_code));
+              cb({}, err);
+              return;
+          }
 
-                  emit userKeysUpdate(last_changed, res);
-          });
+          emit userKeysUpdate(last_changed, res);
+      });
 }
 
 void
 to_json(json &j, const VerificationCache &info)
 {
-        j["device_verified"] = info.device_verified;
-        j["device_blocked"]  = info.device_blocked;
+    j["device_verified"] = info.device_verified;
+    j["device_blocked"]  = info.device_blocked;
 }
 
 void
 from_json(const json &j, VerificationCache &info)
 {
-        info.device_verified = j.at("device_verified").get>();
-        info.device_blocked  = j.at("device_blocked").get>();
+    info.device_verified = j.at("device_verified").get>();
+    info.device_blocked  = j.at("device_blocked").get>();
+}
+
+void
+to_json(json &j, const OnlineBackupVersion &info)
+{
+    j["v"] = info.version;
+    j["a"] = info.algorithm;
+}
+
+void
+from_json(const json &j, OnlineBackupVersion &info)
+{
+    info.version   = j.at("v").get();
+    info.algorithm = j.at("a").get();
 }
 
 std::optional
 Cache::verificationCache(const std::string &user_id, lmdb::txn &txn)
 {
-        std::string_view verifiedVal;
+    std::string_view verifiedVal;
 
-        auto db = getVerificationDb(txn);
+    auto db = getVerificationDb(txn);
 
-        try {
-                VerificationCache verified_state;
-                auto res = db.get(txn, user_id, verifiedVal);
-                if (res) {
-                        verified_state = json::parse(verifiedVal);
-                        return verified_state;
-                } else {
-                        return {};
-                }
-        } catch (std::exception &) {
-                return {};
+    try {
+        VerificationCache verified_state;
+        auto res = db.get(txn, user_id, verifiedVal);
+        if (res) {
+            verified_state = json::parse(verifiedVal);
+            return verified_state;
+        } else {
+            return {};
         }
+    } catch (std::exception &) {
+        return {};
+    }
 }
 
 void
 Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
 {
-        {
-                std::string_view val;
-
-                auto txn = lmdb::txn::begin(env_);
-                auto db  = getVerificationDb(txn);
-
-                try {
-                        VerificationCache verified_state;
-                        auto res = db.get(txn, user_id, val);
-                        if (res) {
-                                verified_state = json::parse(val);
-                        }
-
-                        for (const auto &device : verified_state.device_verified)
-                                if (device == key)
-                                        return;
-
-                        verified_state.device_verified.insert(key);
-                        db.put(txn, user_id, json(verified_state).dump());
-                        txn.commit();
-                } catch (std::exception &) {
-                }
-        }
-
-        const auto local_user = utils::localUser().toStdString();
-        std::map tmp;
-        {
-                std::unique_lock lock(verification_storage.verification_storage_mtx);
-                if (user_id == local_user) {
-                        std::swap(tmp, verification_storage.status);
-                } else {
-                        verification_storage.status.erase(user_id);
-                }
-        }
-        if (user_id == local_user) {
-                for (const auto &[user, status] : tmp) {
-                        (void)status;
-                        emit verificationStatusChanged(user);
-                }
-        } else {
-                emit verificationStatusChanged(user_id);
-        }
-}
-
-void
-Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
-{
+    {
         std::string_view val;
 
         auto txn = lmdb::txn::begin(env_);
         auto db  = getVerificationDb(txn);
 
         try {
-                VerificationCache verified_state;
-                auto res = db.get(txn, user_id, val);
-                if (res) {
-                        verified_state = json::parse(val);
-                }
+            VerificationCache verified_state;
+            auto res = db.get(txn, user_id, val);
+            if (res) {
+                verified_state = json::parse(val);
+            }
 
-                verified_state.device_verified.erase(key);
+            for (const auto &device : verified_state.device_verified)
+                if (device == key)
+                    return;
 
-                db.put(txn, user_id, json(verified_state).dump());
-                txn.commit();
+            verified_state.device_verified.insert(key);
+            db.put(txn, user_id, json(verified_state).dump());
+            txn.commit();
         } catch (std::exception &) {
         }
+    }
 
-        const auto local_user = utils::localUser().toStdString();
-        std::map tmp;
-        {
-                std::unique_lock lock(verification_storage.verification_storage_mtx);
-                if (user_id == local_user) {
-                        std::swap(tmp, verification_storage.status);
-                } else {
-                        verification_storage.status.erase(user_id);
-                }
-        }
+    const auto local_user = utils::localUser().toStdString();
+    std::map tmp;
+    {
+        std::unique_lock lock(verification_storage.verification_storage_mtx);
         if (user_id == local_user) {
-                for (const auto &[user, status] : tmp) {
-                        (void)status;
-                        emit verificationStatusChanged(user);
-                }
+            std::swap(tmp, verification_storage.status);
+            verification_storage.status.clear();
         } else {
-                emit verificationStatusChanged(user_id);
+            verification_storage.status.erase(user_id);
         }
+    }
+    if (user_id == local_user) {
+        for (const auto &[user, status] : tmp) {
+            (void)status;
+            emit verificationStatusChanged(user);
+        }
+    }
+    emit verificationStatusChanged(user_id);
+}
+
+void
+Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
+{
+    std::string_view val;
+
+    auto txn = lmdb::txn::begin(env_);
+    auto db  = getVerificationDb(txn);
+
+    try {
+        VerificationCache verified_state;
+        auto res = db.get(txn, user_id, val);
+        if (res) {
+            verified_state = json::parse(val);
+        }
+
+        verified_state.device_verified.erase(key);
+
+        db.put(txn, user_id, json(verified_state).dump());
+        txn.commit();
+    } catch (std::exception &) {
+    }
+
+    const auto local_user = utils::localUser().toStdString();
+    std::map tmp;
+    {
+        std::unique_lock lock(verification_storage.verification_storage_mtx);
+        if (user_id == local_user) {
+            std::swap(tmp, verification_storage.status);
+        } else {
+            verification_storage.status.erase(user_id);
+        }
+    }
+    if (user_id == local_user) {
+        for (const auto &[user, status] : tmp) {
+            (void)status;
+            emit verificationStatusChanged(user);
+        }
+    }
+    emit verificationStatusChanged(user_id);
 }
 
 VerificationStatus
 Cache::verificationStatus(const std::string &user_id)
 {
-        auto txn = ro_txn(env_);
-        return verificationStatus_(user_id, txn);
+    auto txn = ro_txn(env_);
+    return verificationStatus_(user_id, txn);
 }
 
 VerificationStatus
 Cache::verificationStatus_(const std::string &user_id, lmdb::txn &txn)
 {
-        std::unique_lock lock(verification_storage.verification_storage_mtx);
-        if (verification_storage.status.count(user_id))
-                return verification_storage.status.at(user_id);
+    std::unique_lock lock(verification_storage.verification_storage_mtx);
+    if (verification_storage.status.count(user_id))
+        return verification_storage.status.at(user_id);
 
-        VerificationStatus status;
+    VerificationStatus status;
 
-        // assume there is at least one unverified device until we have checked we have the device
-        // list for that user.
-        status.unverified_device_count = 1;
-        status.no_keys                 = true;
+    // assume there is at least one unverified device until we have checked we have the device
+    // list for that user.
+    status.unverified_device_count = 1;
+    status.no_keys                 = true;
 
-        if (auto verifCache = verificationCache(user_id, txn)) {
-                status.verified_devices = verifCache->device_verified;
+    if (auto verifCache = verificationCache(user_id, txn)) {
+        status.verified_devices = verifCache->device_verified;
+    }
+
+    const auto local_user = utils::localUser().toStdString();
+
+    crypto::Trust trustlevel = crypto::Trust::Unverified;
+    if (user_id == local_user) {
+        status.verified_devices.insert(http::client()->device_id());
+        trustlevel = crypto::Trust::Verified;
+    }
+
+    auto verifyAtLeastOneSig = [](const auto &toVerif,
+                                  const std::map &keys,
+                                  const std::string &keyOwner) {
+        if (!toVerif.signatures.count(keyOwner))
+            return false;
+
+        for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
+            if (!keys.count(key_id))
+                continue;
+
+            if (mtx::crypto::ed25519_verify_signature(keys.at(key_id), json(toVerif), signature))
+                return true;
+        }
+        return false;
+    };
+
+    auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) {
+        int currentVerifiedDevices = 0;
+        for (auto device_id : status.verified_devices) {
+            if (theirDeviceKeys.count(device_id))
+                currentVerifiedDevices++;
+        }
+        status.unverified_device_count =
+          static_cast(theirDeviceKeys.size()) - currentVerifiedDevices;
+    };
+
+    try {
+        // for local user verify this device_key -> our master_key -> our self_signing_key
+        // -> our device_keys
+        //
+        // for other user verify this device_key -> our master_key -> our user_signing_key
+        // -> their master_key -> their self_signing_key -> their device_keys
+        //
+        // This means verifying the other user adds 2 extra steps,verifying our user_signing
+        // key and their master key
+        auto ourKeys   = userKeys_(local_user, txn);
+        auto theirKeys = userKeys_(user_id, txn);
+        if (theirKeys)
+            status.no_keys = false;
+
+        if (!ourKeys || !theirKeys) {
+            verification_storage.status[user_id] = status;
+            return status;
         }
 
-        const auto local_user = utils::localUser().toStdString();
+        // Update verified devices count to count without cross-signing
+        updateUnverifiedDevices(theirKeys->device_keys);
 
-        crypto::Trust trustlevel = crypto::Trust::Unverified;
-        if (user_id == local_user) {
-                status.verified_devices.insert(http::client()->device_id());
+        {
+            auto &mk           = ourKeys->master_keys;
+            std::string dev_id = "ed25519:" + http::client()->device_id();
+            if (!mk.signatures.count(local_user) || !mk.signatures.at(local_user).count(dev_id) ||
+                !mtx::crypto::ed25519_verify_signature(olm::client()->identity_keys().ed25519,
+                                                       json(mk),
+                                                       mk.signatures.at(local_user).at(dev_id))) {
+                nhlog::crypto()->debug("We have not verified our own master key");
+                verification_storage.status[user_id] = status;
+                return status;
+            }
+        }
+
+        auto master_keys = ourKeys->master_keys.keys;
+
+        if (user_id != local_user) {
+            bool theirMasterKeyVerified =
+              verifyAtLeastOneSig(ourKeys->user_signing_keys, master_keys, local_user) &&
+              verifyAtLeastOneSig(
+                theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user);
+
+            if (theirMasterKeyVerified)
                 trustlevel = crypto::Trust::Verified;
-        }
-
-        auto verifyAtLeastOneSig = [](const auto &toVerif,
-                                      const std::map &keys,
-                                      const std::string &keyOwner) {
-                if (!toVerif.signatures.count(keyOwner))
-                        return false;
-
-                for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
-                        if (!keys.count(key_id))
-                                continue;
-
-                        if (mtx::crypto::ed25519_verify_signature(
-                              keys.at(key_id), json(toVerif), signature))
-                                return true;
-                }
-                return false;
-        };
-
-        auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) {
-                int currentVerifiedDevices = 0;
-                for (auto device_id : status.verified_devices) {
-                        if (theirDeviceKeys.count(device_id))
-                                currentVerifiedDevices++;
-                }
-                status.unverified_device_count =
-                  static_cast(theirDeviceKeys.size()) - currentVerifiedDevices;
-        };
-
-        try {
-                // for local user verify this device_key -> our master_key -> our self_signing_key
-                // -> our device_keys
-                //
-                // for other user verify this device_key -> our master_key -> our user_signing_key
-                // -> their master_key -> their self_signing_key -> their device_keys
-                //
-                // This means verifying the other user adds 2 extra steps,verifying our user_signing
-                // key and their master key
-                auto ourKeys   = userKeys_(local_user, txn);
-                auto theirKeys = userKeys_(user_id, txn);
-                if (theirKeys)
-                        status.no_keys = false;
-
-                if (!ourKeys || !theirKeys) {
-                        verification_storage.status[user_id] = status;
-                        return status;
-                }
-
-                // Update verified devices count to count without cross-signing
-                updateUnverifiedDevices(theirKeys->device_keys);
-
-                if (!mtx::crypto::ed25519_verify_signature(
-                      olm::client()->identity_keys().ed25519,
-                      json(ourKeys->master_keys),
-                      ourKeys->master_keys.signatures.at(local_user)
-                        .at("ed25519:" + http::client()->device_id()))) {
-                        verification_storage.status[user_id] = status;
-                        return status;
-                }
-
-                auto master_keys = ourKeys->master_keys.keys;
-
-                if (user_id != local_user) {
-                        bool theirMasterKeyVerified =
-                          verifyAtLeastOneSig(
-                            ourKeys->user_signing_keys, master_keys, local_user) &&
-                          verifyAtLeastOneSig(
-                            theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user);
-
-                        if (theirMasterKeyVerified)
-                                trustlevel = crypto::Trust::Verified;
-                        else if (!theirKeys->master_key_changed)
-                                trustlevel = crypto::Trust::TOFU;
-                        else {
-                                verification_storage.status[user_id] = status;
-                                return status;
-                        }
-
-                        master_keys = theirKeys->master_keys.keys;
-                }
-
-                status.user_verified = trustlevel;
-
-                verification_storage.status[user_id] = status;
-                if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
-                        return status;
-
-                for (const auto &[device, device_key] : theirKeys->device_keys) {
-                        (void)device;
-                        try {
-                                auto identkey =
-                                  device_key.keys.at("curve25519:" + device_key.device_id);
-                                if (verifyAtLeastOneSig(
-                                      device_key, theirKeys->self_signing_keys.keys, user_id)) {
-                                        status.verified_devices.insert(device_key.device_id);
-                                        status.verified_device_keys[identkey] = trustlevel;
-                                }
-                        } catch (...) {
-                        }
-                }
-
-                updateUnverifiedDevices(theirKeys->device_keys);
+            else if (!theirKeys->master_key_changed)
+                trustlevel = crypto::Trust::TOFU;
+            else {
                 verification_storage.status[user_id] = status;
                 return status;
-        } catch (std::exception &e) {
-                nhlog::db()->error(
-                  "Failed to calculate verification status of {}: {}", user_id, e.what());
-                return status;
+            }
+
+            master_keys = theirKeys->master_keys.keys;
         }
+
+        status.user_verified = trustlevel;
+
+        verification_storage.status[user_id] = status;
+        if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
+            return status;
+
+        for (const auto &[device, device_key] : theirKeys->device_keys) {
+            (void)device;
+            try {
+                auto identkey = device_key.keys.at("curve25519:" + device_key.device_id);
+                if (verifyAtLeastOneSig(device_key, theirKeys->self_signing_keys.keys, user_id)) {
+                    status.verified_devices.insert(device_key.device_id);
+                    status.verified_device_keys[identkey] = trustlevel;
+                }
+            } catch (...) {
+            }
+        }
+
+        updateUnverifiedDevices(theirKeys->device_keys);
+        verification_storage.status[user_id] = status;
+        return status;
+    } catch (std::exception &e) {
+        nhlog::db()->error("Failed to calculate verification status of {}: {}", user_id, e.what());
+        return status;
+    }
 }
 
 void
 to_json(json &j, const RoomInfo &info)
 {
-        j["name"]         = info.name;
-        j["topic"]        = info.topic;
-        j["avatar_url"]   = info.avatar_url;
-        j["version"]      = info.version;
-        j["is_invite"]    = info.is_invite;
-        j["is_space"]     = info.is_space;
-        j["join_rule"]    = info.join_rule;
-        j["guest_access"] = info.guest_access;
+    j["name"]         = info.name;
+    j["topic"]        = info.topic;
+    j["avatar_url"]   = info.avatar_url;
+    j["version"]      = info.version;
+    j["is_invite"]    = info.is_invite;
+    j["is_space"]     = info.is_space;
+    j["join_rule"]    = info.join_rule;
+    j["guest_access"] = info.guest_access;
 
-        if (info.member_count != 0)
-                j["member_count"] = info.member_count;
+    if (info.member_count != 0)
+        j["member_count"] = info.member_count;
 
-        if (info.tags.size() != 0)
-                j["tags"] = info.tags;
+    if (info.tags.size() != 0)
+        j["tags"] = info.tags;
 }
 
 void
 from_json(const json &j, RoomInfo &info)
 {
-        info.name       = j.at("name");
-        info.topic      = j.at("topic");
-        info.avatar_url = j.at("avatar_url");
-        info.version    = j.value(
-          "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString());
-        info.is_invite    = j.at("is_invite");
-        info.is_space     = j.value("is_space", false);
-        info.join_rule    = j.at("join_rule");
-        info.guest_access = j.at("guest_access");
+    info.name       = j.at("name");
+    info.topic      = j.at("topic");
+    info.avatar_url = j.at("avatar_url");
+    info.version    = j.value(
+      "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString());
+    info.is_invite    = j.at("is_invite");
+    info.is_space     = j.value("is_space", false);
+    info.join_rule    = j.at("join_rule");
+    info.guest_access = j.at("guest_access");
 
-        if (j.count("member_count"))
-                info.member_count = j.at("member_count");
+    if (j.count("member_count"))
+        info.member_count = j.at("member_count");
 
-        if (j.count("tags"))
-                info.tags = j.at("tags").get>();
+    if (j.count("tags"))
+        info.tags = j.at("tags").get>();
 }
 
 void
 to_json(json &j, const ReadReceiptKey &key)
 {
-        j = json{{"event_id", key.event_id}, {"room_id", key.room_id}};
+    j = json{{"event_id", key.event_id}, {"room_id", key.room_id}};
 }
 
 void
 from_json(const json &j, ReadReceiptKey &key)
 {
-        key.event_id = j.at("event_id").get();
-        key.room_id  = j.at("room_id").get();
+    key.event_id = j.at("event_id").get();
+    key.room_id  = j.at("room_id").get();
 }
 
 void
 to_json(json &j, const MemberInfo &info)
 {
-        j["name"]       = info.name;
-        j["avatar_url"] = info.avatar_url;
+    j["name"]       = info.name;
+    j["avatar_url"] = info.avatar_url;
 }
 
 void
 from_json(const json &j, MemberInfo &info)
 {
-        info.name       = j.at("name");
-        info.avatar_url = j.at("avatar_url");
+    info.name       = j.at("name");
+    info.avatar_url = j.at("avatar_url");
 }
 
 void
 to_json(nlohmann::json &obj, const DeviceKeysToMsgIndex &msg)
 {
-        obj["deviceids"] = msg.deviceids;
+    obj["deviceids"] = msg.deviceids;
 }
 
 void
 from_json(const nlohmann::json &obj, DeviceKeysToMsgIndex &msg)
 {
-        msg.deviceids = obj.at("deviceids").get();
+    msg.deviceids = obj.at("deviceids").get();
 }
 
 void
 to_json(nlohmann::json &obj, const SharedWithUsers &msg)
 {
-        obj["keys"] = msg.keys;
+    obj["keys"] = msg.keys;
 }
 
 void
 from_json(const nlohmann::json &obj, SharedWithUsers &msg)
 {
-        msg.keys = obj.at("keys").get>();
+    msg.keys = obj.at("keys").get>();
 }
 
 void
 to_json(nlohmann::json &obj, const GroupSessionData &msg)
 {
-        obj["message_index"] = msg.message_index;
-        obj["ts"]            = msg.timestamp;
+    obj["message_index"] = msg.message_index;
+    obj["ts"]            = msg.timestamp;
+    obj["trust"]         = msg.trusted;
 
-        obj["sender_claimed_ed25519_key"]      = msg.sender_claimed_ed25519_key;
-        obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain;
+    obj["sender_claimed_ed25519_key"]      = msg.sender_claimed_ed25519_key;
+    obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain;
 
-        obj["currently"] = msg.currently;
+    obj["currently"] = msg.currently;
 
-        obj["indices"] = msg.indices;
+    obj["indices"] = msg.indices;
 }
 
 void
 from_json(const nlohmann::json &obj, GroupSessionData &msg)
 {
-        msg.message_index = obj.at("message_index");
-        msg.timestamp     = obj.value("ts", 0ULL);
+    msg.message_index = obj.at("message_index");
+    msg.timestamp     = obj.value("ts", 0ULL);
+    msg.trusted       = obj.value("trust", true);
 
-        msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", "");
-        msg.forwarding_curve25519_key_chain =
-          obj.value("forwarding_curve25519_key_chain", std::vector{});
+    msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", "");
+    msg.forwarding_curve25519_key_chain =
+      obj.value("forwarding_curve25519_key_chain", std::vector{});
 
-        msg.currently = obj.value("currently", SharedWithUsers{});
+    msg.currently = obj.value("currently", SharedWithUsers{});
 
-        msg.indices = obj.value("indices", std::map());
+    msg.indices = obj.value("indices", std::map());
 }
 
 void
 to_json(nlohmann::json &obj, const DevicePublicKeys &msg)
 {
-        obj["ed25519"]    = msg.ed25519;
-        obj["curve25519"] = msg.curve25519;
+    obj["ed25519"]    = msg.ed25519;
+    obj["curve25519"] = msg.curve25519;
 }
 
 void
 from_json(const nlohmann::json &obj, DevicePublicKeys &msg)
 {
-        msg.ed25519    = obj.at("ed25519");
-        msg.curve25519 = obj.at("curve25519");
+    msg.ed25519    = obj.at("ed25519");
+    msg.curve25519 = obj.at("curve25519");
 }
 
 void
 to_json(nlohmann::json &obj, const MegolmSessionIndex &msg)
 {
-        obj["room_id"]    = msg.room_id;
-        obj["session_id"] = msg.session_id;
-        obj["sender_key"] = msg.sender_key;
+    obj["room_id"]    = msg.room_id;
+    obj["session_id"] = msg.session_id;
+    obj["sender_key"] = msg.sender_key;
 }
 
 void
 from_json(const nlohmann::json &obj, MegolmSessionIndex &msg)
 {
-        msg.room_id    = obj.at("room_id");
-        msg.session_id = obj.at("session_id");
-        msg.sender_key = obj.at("sender_key");
+    msg.room_id    = obj.at("room_id");
+    msg.session_id = obj.at("session_id");
+    msg.sender_key = obj.at("sender_key");
 }
 
 void
 to_json(nlohmann::json &obj, const StoredOlmSession &msg)
 {
-        obj["ts"] = msg.last_message_ts;
-        obj["s"]  = msg.pickled_session;
+    obj["ts"] = msg.last_message_ts;
+    obj["s"]  = msg.pickled_session;
 }
 void
 from_json(const nlohmann::json &obj, StoredOlmSession &msg)
 {
-        msg.last_message_ts = obj.at("ts").get();
-        msg.pickled_session = obj.at("s").get();
+    msg.last_message_ts = obj.at("ts").get();
+    msg.pickled_session = obj.at("s").get();
 }
 
 namespace cache {
 void
 init(const QString &user_id)
 {
-        qRegisterMetaType();
-        qRegisterMetaType();
-        qRegisterMetaType();
-        qRegisterMetaType>();
-        qRegisterMetaType>();
-        qRegisterMetaType>();
-        qRegisterMetaType();
+    qRegisterMetaType();
+    qRegisterMetaType();
+    qRegisterMetaType();
+    qRegisterMetaType>();
+    qRegisterMetaType>();
+    qRegisterMetaType>();
+    qRegisterMetaType();
 
-        instance_ = std::make_unique(user_id);
+    instance_ = std::make_unique(user_id);
 }
 
 Cache *
 client()
 {
-        return instance_.get();
+    return instance_.get();
 }
 
 std::string
 displayName(const std::string &room_id, const std::string &user_id)
 {
-        return instance_->displayName(room_id, user_id);
+    return instance_->displayName(room_id, user_id);
 }
 
 QString
 displayName(const QString &room_id, const QString &user_id)
 {
-        return instance_->displayName(room_id, user_id);
+    return instance_->displayName(room_id, user_id);
 }
 QString
 avatarUrl(const QString &room_id, const QString &user_id)
 {
-        return instance_->avatarUrl(room_id, user_id);
+    return instance_->avatarUrl(room_id, user_id);
 }
 
 mtx::presence::PresenceState
 presenceState(const std::string &user_id)
 {
-        if (!instance_)
-                return {};
-        return instance_->presenceState(user_id);
+    if (!instance_)
+        return {};
+    return instance_->presenceState(user_id);
 }
 std::string
 statusMessage(const std::string &user_id)
 {
-        return instance_->statusMessage(user_id);
+    return instance_->statusMessage(user_id);
 }
 
 // user cache stores user keys
 std::optional
 userKeys(const std::string &user_id)
 {
-        return instance_->userKeys(user_id);
+    return instance_->userKeys(user_id);
 }
 void
 updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
 {
-        instance_->updateUserKeys(sync_token, keyQuery);
+    instance_->updateUserKeys(sync_token, keyQuery);
 }
 
 // device & user verification cache
 std::optional
 verificationStatus(const std::string &user_id)
 {
-        return instance_->verificationStatus(user_id);
+    return instance_->verificationStatus(user_id);
 }
 
 void
 markDeviceVerified(const std::string &user_id, const std::string &device)
 {
-        instance_->markDeviceVerified(user_id, device);
+    instance_->markDeviceVerified(user_id, device);
 }
 
 void
 markDeviceUnverified(const std::string &user_id, const std::string &device)
 {
-        instance_->markDeviceUnverified(user_id, device);
+    instance_->markDeviceUnverified(user_id, device);
 }
 
 std::vector
 joinedRooms()
 {
-        return instance_->joinedRooms();
+    return instance_->joinedRooms();
 }
 
 QMap
 roomInfo(bool withInvites)
 {
-        return instance_->roomInfo(withInvites);
+    return instance_->roomInfo(withInvites);
 }
 QHash
 invites()
 {
-        return instance_->invites();
+    return instance_->invites();
 }
 
 QString
 getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        return instance_->getRoomName(txn, statesdb, membersdb);
+    return instance_->getRoomName(txn, statesdb, membersdb);
 }
 mtx::events::state::JoinRule
 getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        return instance_->getRoomJoinRule(txn, statesdb);
+    return instance_->getRoomJoinRule(txn, statesdb);
 }
 bool
 getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        return instance_->getRoomGuestAccess(txn, statesdb);
+    return instance_->getRoomGuestAccess(txn, statesdb);
 }
 QString
 getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb)
 {
-        return instance_->getRoomTopic(txn, statesdb);
+    return instance_->getRoomTopic(txn, statesdb);
 }
 QString
 getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
 {
-        return instance_->getRoomAvatarUrl(txn, statesdb, membersdb);
+    return instance_->getRoomAvatarUrl(txn, statesdb, membersdb);
 }
 
 std::vector
 getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
 {
-        return instance_->getMembers(room_id, startIndex, len);
+    return instance_->getMembers(room_id, startIndex, len);
+}
+
+std::vector
+getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len)
+{
+    return instance_->getMembersFromInvite(room_id, startIndex, len);
 }
 
 void
 saveState(const mtx::responses::Sync &res)
 {
-        instance_->saveState(res);
+    instance_->saveState(res);
 }
 bool
 isInitialized()
 {
-        return instance_->isInitialized();
+    return instance_->isInitialized();
 }
 
 std::string
 nextBatchToken()
 {
-        return instance_->nextBatchToken();
+    return instance_->nextBatchToken();
 }
 
 void
 deleteData()
 {
-        instance_->deleteData();
+    instance_->deleteData();
 }
 
 void
 removeInvite(lmdb::txn &txn, const std::string &room_id)
 {
-        instance_->removeInvite(txn, room_id);
+    instance_->removeInvite(txn, room_id);
 }
 void
 removeInvite(const std::string &room_id)
 {
-        instance_->removeInvite(room_id);
+    instance_->removeInvite(room_id);
 }
 void
 removeRoom(lmdb::txn &txn, const std::string &roomid)
 {
-        instance_->removeRoom(txn, roomid);
+    instance_->removeRoom(txn, roomid);
 }
 void
 removeRoom(const std::string &roomid)
 {
-        instance_->removeRoom(roomid);
+    instance_->removeRoom(roomid);
 }
 void
 removeRoom(const QString &roomid)
 {
-        instance_->removeRoom(roomid.toStdString());
+    instance_->removeRoom(roomid.toStdString());
 }
 void
 setup()
 {
-        instance_->setup();
+    instance_->setup();
 }
 
 bool
 runMigrations()
 {
-        return instance_->runMigrations();
+    return instance_->runMigrations();
 }
 
 cache::CacheVersion
 formatVersion()
 {
-        return instance_->formatVersion();
+    return instance_->formatVersion();
 }
 
 void
 setCurrentFormat()
 {
-        instance_->setCurrentFormat();
+    instance_->setCurrentFormat();
 }
 
 std::vector
 roomIds()
 {
-        return instance_->roomIds();
+    return instance_->roomIds();
 }
 
 QMap
 getTimelineMentions()
 {
-        return instance_->getTimelineMentions();
+    return instance_->getTimelineMentions();
 }
 
 //! Retrieve all the user ids from a room.
 std::vector
 roomMembers(const std::string &room_id)
 {
-        return instance_->roomMembers(room_id);
+    return instance_->roomMembers(room_id);
 }
 
 //! Check if the given user has power leve greater than than
@@ -4757,48 +4861,48 @@ hasEnoughPowerLevel(const std::vector &eventTypes,
                     const std::string &room_id,
                     const std::string &user_id)
 {
-        return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id);
+    return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id);
 }
 
 void
 updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts)
 {
-        instance_->updateReadReceipt(txn, room_id, receipts);
+    instance_->updateReadReceipt(txn, room_id, receipts);
 }
 
 UserReceipts
 readReceipts(const QString &event_id, const QString &room_id)
 {
-        return instance_->readReceipts(event_id, room_id);
+    return instance_->readReceipts(event_id, room_id);
 }
 
 std::optional
 getEventIndex(const std::string &room_id, std::string_view event_id)
 {
-        return instance_->getEventIndex(room_id, event_id);
+    return instance_->getEventIndex(room_id, event_id);
 }
 
 std::optional>
 lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id)
 {
-        return instance_->lastInvisibleEventAfter(room_id, event_id);
+    return instance_->lastInvisibleEventAfter(room_id, event_id);
 }
 
 RoomInfo
 singleRoomInfo(const std::string &room_id)
 {
-        return instance_->singleRoomInfo(room_id);
+    return instance_->singleRoomInfo(room_id);
 }
 std::vector
 roomsWithStateUpdates(const mtx::responses::Sync &res)
 {
-        return instance_->roomsWithStateUpdates(res);
+    return instance_->roomsWithStateUpdates(res);
 }
 
 std::map
 getRoomInfo(const std::vector &rooms)
 {
-        return instance_->getRoomInfo(rooms);
+    return instance_->getRoomInfo(rooms);
 }
 
 //! Calculates which the read status of a room.
@@ -4806,74 +4910,74 @@ getRoomInfo(const std::vector &rooms)
 bool
 calculateRoomReadStatus(const std::string &room_id)
 {
-        return instance_->calculateRoomReadStatus(room_id);
+    return instance_->calculateRoomReadStatus(room_id);
 }
 void
 calculateRoomReadStatus()
 {
-        instance_->calculateRoomReadStatus();
+    instance_->calculateRoomReadStatus();
 }
 
 void
 markSentNotification(const std::string &event_id)
 {
-        instance_->markSentNotification(event_id);
+    instance_->markSentNotification(event_id);
 }
 //! Removes an event from the sent notifications.
 void
 removeReadNotification(const std::string &event_id)
 {
-        instance_->removeReadNotification(event_id);
+    instance_->removeReadNotification(event_id);
 }
 //! Check if we have sent a desktop notification for the given event id.
 bool
 isNotificationSent(const std::string &event_id)
 {
-        return instance_->isNotificationSent(event_id);
+    return instance_->isNotificationSent(event_id);
 }
 
 //! Add all notifications containing a user mention to the db.
 void
 saveTimelineMentions(const mtx::responses::Notifications &res)
 {
-        instance_->saveTimelineMentions(res);
+    instance_->saveTimelineMentions(res);
 }
 
 //! Remove old unused data.
 void
 deleteOldMessages()
 {
-        instance_->deleteOldMessages();
+    instance_->deleteOldMessages();
 }
 void
 deleteOldData() noexcept
 {
-        instance_->deleteOldData();
+    instance_->deleteOldData();
 }
 //! Retrieve all saved room ids.
 std::vector
 getRoomIds(lmdb::txn &txn)
 {
-        return instance_->getRoomIds(txn);
+    return instance_->getRoomIds(txn);
 }
 
 //! Mark a room that uses e2e encryption.
 void
 setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
 {
-        instance_->setEncryptedRoom(txn, room_id);
+    instance_->setEncryptedRoom(txn, room_id);
 }
 bool
 isRoomEncrypted(const std::string &room_id)
 {
-        return instance_->isRoomEncrypted(room_id);
+    return instance_->isRoomEncrypted(room_id);
 }
 
 //! Check if a user is a member of the room.
 bool
 isRoomMember(const std::string &user_id, const std::string &room_id)
 {
-        return instance_->isRoomMember(user_id, room_id);
+    return instance_->isRoomMember(user_id, room_id);
 }
 
 //
@@ -4884,40 +4988,40 @@ saveOutboundMegolmSession(const std::string &room_id,
                           const GroupSessionData &data,
                           mtx::crypto::OutboundGroupSessionPtr &session)
 {
-        instance_->saveOutboundMegolmSession(room_id, data, session);
+    instance_->saveOutboundMegolmSession(room_id, data, session);
 }
 OutboundGroupSessionDataRef
 getOutboundMegolmSession(const std::string &room_id)
 {
-        return instance_->getOutboundMegolmSession(room_id);
+    return instance_->getOutboundMegolmSession(room_id);
 }
 bool
 outboundMegolmSessionExists(const std::string &room_id) noexcept
 {
-        return instance_->outboundMegolmSessionExists(room_id);
+    return instance_->outboundMegolmSessionExists(room_id);
 }
 void
 updateOutboundMegolmSession(const std::string &room_id,
                             const GroupSessionData &data,
                             mtx::crypto::OutboundGroupSessionPtr &session)
 {
-        instance_->updateOutboundMegolmSession(room_id, data, session);
+    instance_->updateOutboundMegolmSession(room_id, data, session);
 }
 void
 dropOutboundMegolmSession(const std::string &room_id)
 {
-        instance_->dropOutboundMegolmSession(room_id);
+    instance_->dropOutboundMegolmSession(room_id);
 }
 
 void
 importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
 {
-        instance_->importSessionKeys(keys);
+    instance_->importSessionKeys(keys);
 }
 mtx::crypto::ExportedSessionKeys
 exportSessionKeys()
 {
-        return instance_->exportSessionKeys();
+    return instance_->exportSessionKeys();
 }
 
 //
@@ -4928,22 +5032,22 @@ saveInboundMegolmSession(const MegolmSessionIndex &index,
                          mtx::crypto::InboundGroupSessionPtr session,
                          const GroupSessionData &data)
 {
-        instance_->saveInboundMegolmSession(index, std::move(session), data);
+    instance_->saveInboundMegolmSession(index, std::move(session), data);
 }
 mtx::crypto::InboundGroupSessionPtr
 getInboundMegolmSession(const MegolmSessionIndex &index)
 {
-        return instance_->getInboundMegolmSession(index);
+    return instance_->getInboundMegolmSession(index);
 }
 bool
 inboundMegolmSessionExists(const MegolmSessionIndex &index)
 {
-        return instance_->inboundMegolmSessionExists(index);
+    return instance_->inboundMegolmSessionExists(index);
 }
 std::optional
 getMegolmSessionData(const MegolmSessionIndex &index)
 {
-        return instance_->getMegolmSessionData(index);
+    return instance_->getMegolmSessionData(index);
 }
 
 //
@@ -4954,43 +5058,43 @@ saveOlmSession(const std::string &curve25519,
                mtx::crypto::OlmSessionPtr session,
                uint64_t timestamp)
 {
-        instance_->saveOlmSession(curve25519, std::move(session), timestamp);
+    instance_->saveOlmSession(curve25519, std::move(session), timestamp);
 }
 std::vector
 getOlmSessions(const std::string &curve25519)
 {
-        return instance_->getOlmSessions(curve25519);
+    return instance_->getOlmSessions(curve25519);
 }
 std::optional
 getOlmSession(const std::string &curve25519, const std::string &session_id)
 {
-        return instance_->getOlmSession(curve25519, session_id);
+    return instance_->getOlmSession(curve25519, session_id);
 }
 std::optional
 getLatestOlmSession(const std::string &curve25519)
 {
-        return instance_->getLatestOlmSession(curve25519);
+    return instance_->getLatestOlmSession(curve25519);
 }
 
 void
 saveOlmAccount(const std::string &pickled)
 {
-        instance_->saveOlmAccount(pickled);
+    instance_->saveOlmAccount(pickled);
 }
 std::string
 restoreOlmAccount()
 {
-        return instance_->restoreOlmAccount();
+    return instance_->restoreOlmAccount();
 }
 
 void
 storeSecret(const std::string &name, const std::string &secret)
 {
-        instance_->storeSecret(name, secret);
+    instance_->storeSecret(name, secret);
 }
 std::optional
 secret(const std::string &name)
 {
-        return instance_->secret(name);
+    return instance_->secret(name);
 }
 } // namespace cache
diff --git a/src/Cache.h b/src/Cache.h
index 57a36d73..f8626430 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -83,6 +83,9 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
 //! Retrieve member info from a room.
 std::vector
 getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30);
+//! Retrive member info from an invite.
+std::vector
+getMembersFromInvite(const std::string &room_id, std::size_t start_index = 0, std::size_t len = 30);
 
 bool
 isInitialized();
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 6c402674..b7461848 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -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 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 deviceids;
 };
 
 struct SharedWithUsers
 {
-        // userid to keys
-        std::map keys;
+    // userid to keys
+    std::map 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 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 indices;
+    std::string sender_claimed_ed25519_key;
+    std::vector 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 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 verified_devices;
-        //! Map from sender key/curve25519 to trust status
-        std::map 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 verified_devices;
+    //! Map from sender key/curve25519 to trust status
+    std::map 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 status;
-        std::mutex verification_storage_mtx;
+    //! mapping of user to verification status
+    std::map 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 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 seen_device_keys;
-        //! Device ids that were already used at least once
-        std::set seen_device_ids;
+    //! Device id to device keys
+    std::map 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 seen_device_keys;
+    //! Device ids that were already used at least once
+    std::set 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 device_verified;
-        //! list of devices the user blocks
-        std::set device_blocked;
+    //! list of verified device_ids with device-verification
+    std::set device_verified;
+    //! list of devices the user blocks
+    std::set 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);
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index 4a5c5c76..e28f5b2d 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -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 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 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;
 };
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 748404d1..651d73d7 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -32,687 +32,660 @@
 
 class Cache : public QObject
 {
-        Q_OBJECT
+    Q_OBJECT
 
 public:
-        Cache(const QString &userId, QObject *parent = nullptr);
+    Cache(const QString &userId, QObject *parent = nullptr);
 
-        std::string displayName(const std::string &room_id, const std::string &user_id);
-        QString displayName(const QString &room_id, const QString &user_id);
-        QString avatarUrl(const QString &room_id, const QString &user_id);
+    std::string displayName(const std::string &room_id, const std::string &user_id);
+    QString displayName(const QString &room_id, const QString &user_id);
+    QString avatarUrl(const QString &room_id, const QString &user_id);
 
-        // presence
-        mtx::presence::PresenceState presenceState(const std::string &user_id);
-        std::string statusMessage(const std::string &user_id);
+    // presence
+    mtx::presence::PresenceState presenceState(const std::string &user_id);
+    std::string statusMessage(const std::string &user_id);
 
-        // user cache stores user keys
-        std::map> getMembersWithKeys(
-          const std::string &room_id,
-          bool verified_only);
-        void updateUserKeys(const std::string &sync_token,
-                            const mtx::responses::QueryKeys &keyQuery);
-        void markUserKeysOutOfDate(lmdb::txn &txn,
-                                   lmdb::dbi &db,
-                                   const std::vector &user_ids,
-                                   const std::string &sync_token);
-        void deleteUserKeys(lmdb::txn &txn,
-                            lmdb::dbi &db,
-                            const std::vector &user_ids);
-        void query_keys(const std::string &user_id,
-                        std::function cb);
+    // user cache stores user keys
+    std::map> getMembersWithKeys(
+      const std::string &room_id,
+      bool verified_only);
+    void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
+    void markUserKeysOutOfDate(const std::vector &user_ids);
+    void markUserKeysOutOfDate(lmdb::txn &txn,
+                               lmdb::dbi &db,
+                               const std::vector &user_ids,
+                               const std::string &sync_token);
+    void query_keys(const std::string &user_id,
+                    std::function cb);
 
-        // device & user verification cache
-        std::optional userKeys(const std::string &user_id);
-        VerificationStatus verificationStatus(const std::string &user_id);
-        void markDeviceVerified(const std::string &user_id, const std::string &device);
-        void markDeviceUnverified(const std::string &user_id, const std::string &device);
-        crypto::Trust roomVerificationStatus(const std::string &room_id);
+    // device & user verification cache
+    std::optional userKeys(const std::string &user_id);
+    VerificationStatus verificationStatus(const std::string &user_id);
+    void markDeviceVerified(const std::string &user_id, const std::string &device);
+    void markDeviceUnverified(const std::string &user_id, const std::string &device);
+    crypto::Trust roomVerificationStatus(const std::string &room_id);
 
-        std::vector joinedRooms();
+    std::vector joinedRooms();
 
-        QMap roomInfo(bool withInvites = true);
-        std::optional getRoomAliases(const std::string &roomid);
-        QHash invites();
-        std::optional invite(std::string_view roomid);
-        QMap> spaces();
+    QMap roomInfo(bool withInvites = true);
+    std::optional getRoomAliases(const std::string &roomid);
+    QHash invites();
+    std::optional invite(std::string_view roomid);
+    QMap> spaces();
 
-        //! Calculate & return the name of the room.
-        QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        //! Get room join rules
-        mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb);
-        bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb);
-        //! Retrieve the topic of the room if any.
-        QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
-        //! Retrieve the room avatar's url if any.
-        QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        //! Retrieve the version of the room if any.
-        QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
-        //! Retrieve if the room is a space
-        bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
+    //! Calculate & return the name of the room.
+    QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    //! Get room join rules
+    mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb);
+    bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb);
+    //! Retrieve the topic of the room if any.
+    QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
+    //! Retrieve the room avatar's url if any.
+    QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    //! Retrieve the version of the room if any.
+    QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
+    //! Retrieve if the room is a space
+    bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
 
-        //! Get a specific state event
-        template
-        std::optional> getStateEvent(const std::string &room_id,
-                                                                std::string_view state_key = "")
-        {
-                auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
-                return getStateEvent(txn, room_id, state_key);
-        }
+    //! Get a specific state event
+    template
+    std::optional> getStateEvent(const std::string &room_id,
+                                                            std::string_view state_key = "")
+    {
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        return getStateEvent(txn, room_id, state_key);
+    }
 
-        //! retrieve a specific event from account data
-        //! pass empty room_id for global account data
-        std::optional getAccountData(
-          mtx::events::EventType type,
-          const std::string &room_id = "");
+    //! retrieve a specific event from account data
+    //! pass empty room_id for global account data
+    std::optional getAccountData(
+      mtx::events::EventType type,
+      const std::string &room_id = "");
 
-        //! Retrieve member info from a room.
-        std::vector getMembers(const std::string &room_id,
-                                           std::size_t startIndex = 0,
-                                           std::size_t len        = 30);
-        size_t memberCount(const std::string &room_id);
+    //! Retrieve member info from a room.
+    std::vector getMembers(const std::string &room_id,
+                                       std::size_t startIndex = 0,
+                                       std::size_t len        = 30);
 
-        void saveState(const mtx::responses::Sync &res);
-        bool isInitialized();
-        bool isDatabaseReady() { return databaseReady_ && isInitialized(); }
+    std::vector getMembersFromInvite(const std::string &room_id,
+                                                 std::size_t startIndex = 0,
+                                                 std::size_t len        = 30);
+    size_t memberCount(const std::string &room_id);
 
-        std::string nextBatchToken();
+    void saveState(const mtx::responses::Sync &res);
+    bool isInitialized();
+    bool isDatabaseReady() { return databaseReady_ && isInitialized(); }
 
-        void deleteData();
+    std::string nextBatchToken();
 
-        void removeInvite(lmdb::txn &txn, const std::string &room_id);
-        void removeInvite(const std::string &room_id);
-        void removeRoom(lmdb::txn &txn, const std::string &roomid);
-        void removeRoom(const std::string &roomid);
-        void setup();
+    void deleteData();
 
-        cache::CacheVersion formatVersion();
-        void setCurrentFormat();
-        bool runMigrations();
+    void removeInvite(lmdb::txn &txn, const std::string &room_id);
+    void removeInvite(const std::string &room_id);
+    void removeRoom(lmdb::txn &txn, const std::string &roomid);
+    void removeRoom(const std::string &roomid);
+    void setup();
 
-        std::vector roomIds();
-        QMap getTimelineMentions();
+    cache::CacheVersion formatVersion();
+    void setCurrentFormat();
+    bool runMigrations();
 
-        //! Retrieve all the user ids from a room.
-        std::vector roomMembers(const std::string &room_id);
+    std::vector roomIds();
+    QMap getTimelineMentions();
 
-        //! Check if the given user has power leve greater than than
-        //! lowest power level of the given events.
-        bool hasEnoughPowerLevel(const std::vector &eventTypes,
+    //! Retrieve all the user ids from a room.
+    std::vector roomMembers(const std::string &room_id);
+
+    //! Check if the given user has power leve greater than than
+    //! lowest power level of the given events.
+    bool hasEnoughPowerLevel(const std::vector &eventTypes,
+                             const std::string &room_id,
+                             const std::string &user_id);
+
+    //! Adds a user to the read list for the given event.
+    //!
+    //! There should be only one user id present in a receipt list per room.
+    //! The user id should be removed from any other lists.
+    using Receipts = std::map>;
+    void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts);
+
+    //! Retrieve all the read receipts for the given event id and room.
+    //!
+    //! Returns a map of user ids and the time of the read receipt in milliseconds.
+    using UserReceipts = std::multimap>;
+    UserReceipts readReceipts(const QString &event_id, const QString &room_id);
+
+    RoomInfo singleRoomInfo(const std::string &room_id);
+    std::vector roomsWithStateUpdates(const mtx::responses::Sync &res);
+    std::map getRoomInfo(const std::vector &rooms);
+
+    //! Calculates which the read status of a room.
+    //! Whether all the events in the timeline have been read.
+    bool calculateRoomReadStatus(const std::string &room_id);
+    void calculateRoomReadStatus();
+
+    void markSentNotification(const std::string &event_id);
+    //! Removes an event from the sent notifications.
+    void removeReadNotification(const std::string &event_id);
+    //! Check if we have sent a desktop notification for the given event id.
+    bool isNotificationSent(const std::string &event_id);
+
+    //! Add all notifications containing a user mention to the db.
+    void saveTimelineMentions(const mtx::responses::Notifications &res);
+
+    //! retrieve events in timeline and related functions
+    struct Messages
+    {
+        mtx::responses::Timeline timeline;
+        uint64_t next_index;
+        bool end_of_cache = false;
+    };
+    Messages getTimelineMessages(lmdb::txn &txn,
                                  const std::string &room_id,
-                                 const std::string &user_id);
+                                 uint64_t index = std::numeric_limits::max(),
+                                 bool forward   = false);
 
-        //! Adds a user to the read list for the given event.
-        //!
-        //! There should be only one user id present in a receipt list per room.
-        //! The user id should be removed from any other lists.
-        using Receipts = std::map>;
-        void updateReadReceipt(lmdb::txn &txn,
-                               const std::string &room_id,
-                               const Receipts &receipts);
+    std::optional getEvent(const std::string &room_id,
+                                                                    const std::string &event_id);
+    void storeEvent(const std::string &room_id,
+                    const std::string &event_id,
+                    const mtx::events::collections::TimelineEvent &event);
+    void replaceEvent(const std::string &room_id,
+                      const std::string &event_id,
+                      const mtx::events::collections::TimelineEvent &event);
+    std::vector relatedEvents(const std::string &room_id, const std::string &event_id);
 
-        //! Retrieve all the read receipts for the given event id and room.
-        //!
-        //! Returns a map of user ids and the time of the read receipt in milliseconds.
-        using UserReceipts = std::multimap>;
-        UserReceipts readReceipts(const QString &event_id, const QString &room_id);
+    struct TimelineRange
+    {
+        uint64_t first, last;
+    };
+    std::optional getTimelineRange(const std::string &room_id);
+    std::optional getTimelineIndex(const std::string &room_id, std::string_view event_id);
+    std::optional getEventIndex(const std::string &room_id, std::string_view event_id);
+    std::optional> lastInvisibleEventAfter(
+      const std::string &room_id,
+      std::string_view event_id);
+    std::optional getTimelineEventId(const std::string &room_id, uint64_t index);
+    std::optional getArrivalIndex(const std::string &room_id, std::string_view event_id);
 
-        RoomInfo singleRoomInfo(const std::string &room_id);
-        std::vector roomsWithStateUpdates(const mtx::responses::Sync &res);
-        std::map getRoomInfo(const std::vector &rooms);
+    std::string previousBatchToken(const std::string &room_id);
+    uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
+    void savePendingMessage(const std::string &room_id,
+                            const mtx::events::collections::TimelineEvent &message);
+    std::optional firstPendingMessage(
+      const std::string &room_id);
+    void removePendingStatus(const std::string &room_id, const std::string &txn_id);
 
-        //! Calculates which the read status of a room.
-        //! Whether all the events in the timeline have been read.
-        bool calculateRoomReadStatus(const std::string &room_id);
-        void calculateRoomReadStatus();
+    //! clear timeline keeping only the latest batch
+    void clearTimeline(const std::string &room_id);
 
-        void markSentNotification(const std::string &event_id);
-        //! Removes an event from the sent notifications.
-        void removeReadNotification(const std::string &event_id);
-        //! Check if we have sent a desktop notification for the given event id.
-        bool isNotificationSent(const std::string &event_id);
+    //! Remove old unused data.
+    void deleteOldMessages();
+    void deleteOldData() noexcept;
+    //! Retrieve all saved room ids.
+    std::vector getRoomIds(lmdb::txn &txn);
+    std::vector getParentRoomIds(const std::string &room_id);
+    std::vector getChildRoomIds(const std::string &room_id);
 
-        //! Add all notifications containing a user mention to the db.
-        void saveTimelineMentions(const mtx::responses::Notifications &res);
+    std::vector getImagePacks(const std::string &room_id,
+                                             std::optional stickers);
 
-        //! retrieve events in timeline and related functions
-        struct Messages
-        {
-                mtx::responses::Timeline timeline;
-                uint64_t next_index;
-                bool end_of_cache = false;
+    //! Mark a room that uses e2e encryption.
+    void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
+    bool isRoomEncrypted(const std::string &room_id);
+    std::optional roomEncryptionSettings(
+      const std::string &room_id);
+
+    //! Check if a user is a member of the room.
+    bool isRoomMember(const std::string &user_id, const std::string &room_id);
+
+    //
+    // Outbound Megolm Sessions
+    //
+    void saveOutboundMegolmSession(const std::string &room_id,
+                                   const GroupSessionData &data,
+                                   mtx::crypto::OutboundGroupSessionPtr &session);
+    OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
+    bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
+    void updateOutboundMegolmSession(const std::string &room_id,
+                                     const GroupSessionData &data,
+                                     mtx::crypto::OutboundGroupSessionPtr &session);
+    void dropOutboundMegolmSession(const std::string &room_id);
+
+    void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
+    mtx::crypto::ExportedSessionKeys exportSessionKeys();
+
+    //
+    // Inbound Megolm Sessions
+    //
+    void saveInboundMegolmSession(const MegolmSessionIndex &index,
+                                  mtx::crypto::InboundGroupSessionPtr session,
+                                  const GroupSessionData &data);
+    mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index);
+    bool inboundMegolmSessionExists(const MegolmSessionIndex &index);
+    std::optional getMegolmSessionData(const MegolmSessionIndex &index);
+
+    //
+    // Olm Sessions
+    //
+    void saveOlmSession(const std::string &curve25519,
+                        mtx::crypto::OlmSessionPtr session,
+                        uint64_t timestamp);
+    std::vector getOlmSessions(const std::string &curve25519);
+    std::optional getOlmSession(const std::string &curve25519,
+                                                            const std::string &session_id);
+    std::optional getLatestOlmSession(const std::string &curve25519);
+
+    void saveOlmAccount(const std::string &pickled);
+    std::string restoreOlmAccount();
+
+    void saveBackupVersion(const OnlineBackupVersion &data);
+    void deleteBackupVersion();
+    std::optional backupVersion();
+
+    void storeSecret(const std::string name, const std::string secret, bool internal = false);
+    void deleteSecret(const std::string name, bool internal = false);
+    std::optional secret(const std::string name, bool internal = false);
+
+    std::string pickleSecret();
+
+    template
+    constexpr static bool isStateEvent_ =
+      std::is_same_v>,
+                     mtx::events::StateEvent().content)>>;
+
+    static int compare_state_key(const MDB_val *a, const MDB_val *b)
+    {
+        auto get_skey = [](const MDB_val *v) {
+            return nlohmann::json::parse(
+                     std::string_view(static_cast(v->mv_data), v->mv_size))
+              .value("key", "");
         };
-        Messages getTimelineMessages(lmdb::txn &txn,
-                                     const std::string &room_id,
-                                     uint64_t index = std::numeric_limits::max(),
-                                     bool forward   = false);
-
-        std::optional getEvent(
-          const std::string &room_id,
-          const std::string &event_id);
-        void storeEvent(const std::string &room_id,
-                        const std::string &event_id,
-                        const mtx::events::collections::TimelineEvent &event);
-        void replaceEvent(const std::string &room_id,
-                          const std::string &event_id,
-                          const mtx::events::collections::TimelineEvent &event);
-        std::vector relatedEvents(const std::string &room_id,
-                                               const std::string &event_id);
-
-        struct TimelineRange
-        {
-                uint64_t first, last;
-        };
-        std::optional getTimelineRange(const std::string &room_id);
-        std::optional getTimelineIndex(const std::string &room_id,
-                                                 std::string_view event_id);
-        std::optional getEventIndex(const std::string &room_id,
-                                              std::string_view event_id);
-        std::optional> lastInvisibleEventAfter(
-          const std::string &room_id,
-          std::string_view event_id);
-        std::optional getTimelineEventId(const std::string &room_id, uint64_t index);
-        std::optional getArrivalIndex(const std::string &room_id,
-                                                std::string_view event_id);
-
-        std::string previousBatchToken(const std::string &room_id);
-        uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
-        void savePendingMessage(const std::string &room_id,
-                                const mtx::events::collections::TimelineEvent &message);
-        std::optional firstPendingMessage(
-          const std::string &room_id);
-        void removePendingStatus(const std::string &room_id, const std::string &txn_id);
-
-        //! clear timeline keeping only the latest batch
-        void clearTimeline(const std::string &room_id);
-
-        //! Remove old unused data.
-        void deleteOldMessages();
-        void deleteOldData() noexcept;
-        //! Retrieve all saved room ids.
-        std::vector getRoomIds(lmdb::txn &txn);
-        std::vector getParentRoomIds(const std::string &room_id);
-        std::vector getChildRoomIds(const std::string &room_id);
-
-        std::vector getImagePacks(const std::string &room_id,
-                                                 std::optional stickers);
-
-        //! Mark a room that uses e2e encryption.
-        void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
-        bool isRoomEncrypted(const std::string &room_id);
-        std::optional roomEncryptionSettings(
-          const std::string &room_id);
-
-        //! Check if a user is a member of the room.
-        bool isRoomMember(const std::string &user_id, const std::string &room_id);
-
-        //
-        // Outbound Megolm Sessions
-        //
-        void saveOutboundMegolmSession(const std::string &room_id,
-                                       const GroupSessionData &data,
-                                       mtx::crypto::OutboundGroupSessionPtr &session);
-        OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
-        bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
-        void updateOutboundMegolmSession(const std::string &room_id,
-                                         const GroupSessionData &data,
-                                         mtx::crypto::OutboundGroupSessionPtr &session);
-        void dropOutboundMegolmSession(const std::string &room_id);
-
-        void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
-        mtx::crypto::ExportedSessionKeys exportSessionKeys();
-
-        //
-        // Inbound Megolm Sessions
-        //
-        void saveInboundMegolmSession(const MegolmSessionIndex &index,
-                                      mtx::crypto::InboundGroupSessionPtr session,
-                                      const GroupSessionData &data);
-        mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(
-          const MegolmSessionIndex &index);
-        bool inboundMegolmSessionExists(const MegolmSessionIndex &index);
-        std::optional getMegolmSessionData(const MegolmSessionIndex &index);
-
-        //
-        // Olm Sessions
-        //
-        void saveOlmSession(const std::string &curve25519,
-                            mtx::crypto::OlmSessionPtr session,
-                            uint64_t timestamp);
-        std::vector getOlmSessions(const std::string &curve25519);
-        std::optional getOlmSession(const std::string &curve25519,
-                                                                const std::string &session_id);
-        std::optional getLatestOlmSession(
-          const std::string &curve25519);
-
-        void saveOlmAccount(const std::string &pickled);
-        std::string restoreOlmAccount();
-
-        void storeSecret(const std::string name, const std::string secret);
-        void deleteSecret(const std::string name);
-        std::optional secret(const std::string name);
-
-        template
-        constexpr static bool isStateEvent_ =
-          std::is_same_v>,
-                         mtx::events::StateEvent().content)>>;
-
-        static int compare_state_key(const MDB_val *a, const MDB_val *b)
-        {
-                auto get_skey = [](const MDB_val *v) {
-                        return nlohmann::json::parse(
-                                 std::string_view(static_cast(v->mv_data),
-                                                  v->mv_size))
-                          .value("key", "");
-                };
-
-                return get_skey(a).compare(get_skey(b));
-        }
 
+        return get_skey(a).compare(get_skey(b));
+    }
 signals:
-        void newReadReceipts(const QString &room_id, const std::vector &event_ids);
-        void roomReadStatus(const std::map &status);
-        void removeNotification(const QString &room_id, const QString &event_id);
-        void userKeysUpdate(const std::string &sync_token,
-                            const mtx::responses::QueryKeys &keyQuery);
-        void verificationStatusChanged(const std::string &userid);
-        void secretChanged(const std::string name);
+    void newReadReceipts(const QString &room_id, const std::vector &event_ids);
+    void roomReadStatus(const std::map &status);
+    void removeNotification(const QString &room_id, const QString &event_id);
+    void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
+    void verificationStatusChanged(const std::string &userid);
+    void selfVerificationStatusChanged();
+    void secretChanged(const std::string name);
 
 private:
-        //! Save an invited room.
-        void saveInvite(lmdb::txn &txn,
+    //! Save an invited room.
+    void saveInvite(lmdb::txn &txn,
+                    lmdb::dbi &statesdb,
+                    lmdb::dbi &membersdb,
+                    const mtx::responses::InvitedRoom &room);
+
+    //! Add a notification containing a user mention to the db.
+    void saveTimelineMentions(lmdb::txn &txn,
+                              const std::string &room_id,
+                              const QList &res);
+
+    //! Get timeline items that a user was mentions in for a given room
+    mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn,
+                                                             const std::string &room_id);
+
+    QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
+    QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+    bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db);
+
+    std::optional getMember(const std::string &room_id, const std::string &user_id);
+
+    std::string getLastEventId(lmdb::txn &txn, const std::string &room_id);
+    void saveTimelineMessages(lmdb::txn &txn,
+                              lmdb::dbi &eventsDb,
+                              const std::string &room_id,
+                              const mtx::responses::Timeline &res);
+
+    //! retrieve a specific event from account data
+    //! pass empty room_id for global account data
+    std::optional
+    getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
+    bool isHiddenEvent(lmdb::txn &txn,
+                       mtx::events::collections::TimelineEvents e,
+                       const std::string &room_id);
+
+    //! Remove a room from the cache.
+    // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
+    template
+    void saveStateEvents(lmdb::txn &txn,
+                         lmdb::dbi &statesdb,
+                         lmdb::dbi &stateskeydb,
+                         lmdb::dbi &membersdb,
+                         lmdb::dbi &eventsDb,
+                         const std::string &room_id,
+                         const std::vector &events)
+    {
+        for (const auto &e : events)
+            saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e);
+    }
+
+    template
+    void saveStateEvent(lmdb::txn &txn,
                         lmdb::dbi &statesdb,
+                        lmdb::dbi &stateskeydb,
                         lmdb::dbi &membersdb,
-                        const mtx::responses::InvitedRoom &room);
+                        lmdb::dbi &eventsDb,
+                        const std::string &room_id,
+                        const T &event)
+    {
+        using namespace mtx::events;
+        using namespace mtx::events::state;
 
-        //! Add a notification containing a user mention to the db.
-        void saveTimelineMentions(lmdb::txn &txn,
-                                  const std::string &room_id,
-                                  const QList &res);
+        if (auto e = std::get_if>(&event); e != nullptr) {
+            switch (e->content.membership) {
+            //
+            // We only keep users with invite or join membership.
+            //
+            case Membership::Invite:
+            case Membership::Join: {
+                auto display_name =
+                  e->content.display_name.empty() ? e->state_key : e->content.display_name;
 
-        //! Get timeline items that a user was mentions in for a given room
-        mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn,
-                                                                 const std::string &room_id);
+                // Lightweight representation of a member.
+                MemberInfo tmp{display_name, e->content.avatar_url};
 
-        QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
-        QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-        bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db);
+                membersdb.put(txn, e->state_key, json(tmp).dump());
+                break;
+            }
+            default: {
+                membersdb.del(txn, e->state_key, "");
+                break;
+            }
+            }
 
-        std::optional getMember(const std::string &room_id, const std::string &user_id);
-
-        std::string getLastEventId(lmdb::txn &txn, const std::string &room_id);
-        void saveTimelineMessages(lmdb::txn &txn,
-                                  lmdb::dbi &eventsDb,
-                                  const std::string &room_id,
-                                  const mtx::responses::Timeline &res);
-
-        //! retrieve a specific event from account data
-        //! pass empty room_id for global account data
-        std::optional
-        getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
-        bool isHiddenEvent(lmdb::txn &txn,
-                           mtx::events::collections::TimelineEvents e,
-                           const std::string &room_id);
-
-        //! Remove a room from the cache.
-        // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
-        template
-        void saveStateEvents(lmdb::txn &txn,
-                             lmdb::dbi &statesdb,
-                             lmdb::dbi &stateskeydb,
-                             lmdb::dbi &membersdb,
-                             lmdb::dbi &eventsDb,
-                             const std::string &room_id,
-                             const std::vector &events)
-        {
-                for (const auto &e : events)
-                        saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e);
+            return;
+        } else if (std::holds_alternative>(event)) {
+            setEncryptedRoom(txn, room_id);
+            return;
         }
 
-        template
-        void saveStateEvent(lmdb::txn &txn,
-                            lmdb::dbi &statesdb,
-                            lmdb::dbi &stateskeydb,
-                            lmdb::dbi &membersdb,
-                            lmdb::dbi &eventsDb,
-                            const std::string &room_id,
-                            const T &event)
+        std::visit(
+          [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
+              if constexpr (isStateEvent_) {
+                  eventsDb.put(txn, e.event_id, json(e).dump());
+
+                  if (e.type != EventType::Unsupported) {
+                      if (std::is_same_v>,
+                                         StateEvent>) {
+                          if (e.type == EventType::RoomMember)
+                              membersdb.del(txn, e.state_key, "");
+                          else if (e.state_key.empty())
+                              statesdb.del(txn, to_string(e.type));
+                          else
+                              stateskeydb.del(txn,
+                                              to_string(e.type),
+                                              json::object({
+                                                             {"key", e.state_key},
+                                                             {"id", e.event_id},
+                                                           })
+                                                .dump());
+                      } else if (e.state_key.empty())
+                          statesdb.put(txn, to_string(e.type), json(e).dump());
+                      else
+                          stateskeydb.put(txn,
+                                          to_string(e.type),
+                                          json::object({
+                                                         {"key", e.state_key},
+                                                         {"id", e.event_id},
+                                                       })
+                                            .dump());
+                  }
+              }
+          },
+          event);
+    }
+
+    template
+    std::optional> getStateEvent(lmdb::txn &txn,
+                                                            const std::string &room_id,
+                                                            std::string_view state_key = "")
+    {
+        constexpr auto type = mtx::events::state_content_to_type;
+        static_assert(type != mtx::events::EventType::Unsupported,
+                      "Not a supported type in state events.");
+
+        if (room_id.empty())
+            return std::nullopt;
+        const auto typeStr = to_string(type);
+
+        std::string_view value;
+        if (state_key.empty()) {
+            auto db = getStatesDb(txn, room_id);
+            if (!db.get(txn, typeStr, value)) {
+                return std::nullopt;
+            }
+        } else {
+            auto db                   = getStatesKeyDb(txn, room_id);
+            std::string d             = json::object({{"key", state_key}}).dump();
+            std::string_view data     = d;
+            std::string_view typeStrV = typeStr;
+
+            auto cursor = lmdb::cursor::open(txn, db);
+            if (!cursor.get(typeStrV, data, MDB_GET_BOTH))
+                return std::nullopt;
+
+            try {
+                auto eventsDb = getEventsDb(txn, room_id);
+                if (!eventsDb.get(txn, json::parse(data)["id"].get(), value))
+                    return std::nullopt;
+            } catch (std::exception &e) {
+                return std::nullopt;
+            }
+        }
+
+        try {
+            return json::parse(value).get>();
+        } catch (std::exception &e) {
+            return std::nullopt;
+        }
+    }
+
+    template
+    std::vector> getStateEventsWithType(lmdb::txn &txn,
+                                                                   const std::string &room_id)
+
+    {
+        constexpr auto type = mtx::events::state_content_to_type;
+        static_assert(type != mtx::events::EventType::Unsupported,
+                      "Not a supported type in state events.");
+
+        if (room_id.empty())
+            return {};
+
+        std::vector> events;
+
         {
-                using namespace mtx::events;
-                using namespace mtx::events::state;
+            auto db                   = getStatesKeyDb(txn, room_id);
+            auto eventsDb             = getEventsDb(txn, room_id);
+            const auto typeStr        = to_string(type);
+            std::string_view typeStrV = typeStr;
+            std::string_view data;
+            std::string_view value;
 
-                if (auto e = std::get_if>(&event); e != nullptr) {
-                        switch (e->content.membership) {
-                        //
-                        // We only keep users with invite or join membership.
-                        //
-                        case Membership::Invite:
-                        case Membership::Join: {
-                                auto display_name = e->content.display_name.empty()
-                                                      ? e->state_key
-                                                      : e->content.display_name;
+            auto cursor = lmdb::cursor::open(txn, db);
+            bool first  = true;
+            if (cursor.get(typeStrV, data, MDB_SET)) {
+                while (cursor.get(typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                    first = false;
 
-                                // Lightweight representation of a member.
-                                MemberInfo tmp{display_name, e->content.avatar_url};
-
-                                membersdb.put(txn, e->state_key, json(tmp).dump());
-                                break;
-                        }
-                        default: {
-                                membersdb.del(txn, e->state_key, "");
-                                break;
-                        }
-                        }
-
-                        return;
-                } else if (std::holds_alternative>(event)) {
-                        setEncryptedRoom(txn, room_id);
-                        return;
+                    if (eventsDb.get(txn, json::parse(data)["id"].get(), value))
+                        events.push_back(json::parse(value).get>());
                 }
-
-                std::visit(
-                  [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
-                          if constexpr (isStateEvent_) {
-                                  eventsDb.put(txn, e.event_id, json(e).dump());
-
-                                  if (e.type != EventType::Unsupported) {
-                                          if (std::is_same_v<
-                                                std::remove_cv_t<
-                                                  std::remove_reference_t>,
-                                                StateEvent>) {
-                                                  if (e.type == EventType::RoomMember)
-                                                          membersdb.del(txn, e.state_key, "");
-                                                  else if (e.state_key.empty())
-                                                          statesdb.del(txn, to_string(e.type));
-                                                  else
-                                                          stateskeydb.del(
-                                                            txn,
-                                                            to_string(e.type),
-                                                            json::object({
-                                                                           {"key", e.state_key},
-                                                                           {"id", e.event_id},
-                                                                         })
-                                                              .dump());
-                                          } else if (e.state_key.empty())
-                                                  statesdb.put(
-                                                    txn, to_string(e.type), json(e).dump());
-                                          else
-                                                  stateskeydb.put(
-                                                    txn,
-                                                    to_string(e.type),
-                                                    json::object({
-                                                                   {"key", e.state_key},
-                                                                   {"id", e.event_id},
-                                                                 })
-                                                      .dump());
-                                  }
-                          }
-                  },
-                  event);
+            }
         }
 
-        template
-        std::optional> getStateEvent(lmdb::txn &txn,
-                                                                const std::string &room_id,
-                                                                std::string_view state_key = "")
-        {
-                constexpr auto type = mtx::events::state_content_to_type;
-                static_assert(type != mtx::events::EventType::Unsupported,
-                              "Not a supported type in state events.");
+        return events;
+    }
+    void saveInvites(lmdb::txn &txn,
+                     const std::map &rooms);
 
-                if (room_id.empty())
-                        return std::nullopt;
-                const auto typeStr = to_string(type);
+    void savePresence(
+      lmdb::txn &txn,
+      const std::vector> &presenceUpdates);
 
-                std::string_view value;
-                if (state_key.empty()) {
-                        auto db = getStatesDb(txn, room_id);
-                        if (!db.get(txn, typeStr, value)) {
-                                return std::nullopt;
-                        }
-                } else {
-                        auto db                   = getStatesKeyDb(txn, room_id);
-                        std::string d             = json::object({{"key", state_key}}).dump();
-                        std::string_view data     = d;
-                        std::string_view typeStrV = typeStr;
+    //! Sends signals for the rooms that are removed.
+    void removeLeftRooms(lmdb::txn &txn,
+                         const std::map &rooms)
+    {
+        for (const auto &room : rooms) {
+            removeRoom(txn, room.first);
 
-                        auto cursor = lmdb::cursor::open(txn, db);
-                        if (!cursor.get(typeStrV, data, MDB_GET_BOTH))
-                                return std::nullopt;
-
-                        try {
-                                auto eventsDb = getEventsDb(txn, room_id);
-                                if (!eventsDb.get(
-                                      txn, json::parse(data)["id"].get(), value))
-                                        return std::nullopt;
-                        } catch (std::exception &e) {
-                                return std::nullopt;
-                        }
-                }
-
-                try {
-                        return json::parse(value).get>();
-                } catch (std::exception &e) {
-                        return std::nullopt;
-                }
+            // Clean up leftover invites.
+            removeInvite(txn, room.first);
         }
+    }
 
-        template
-        std::vector> getStateEventsWithType(lmdb::txn &txn,
-                                                                       const std::string &room_id)
+    void updateSpaces(lmdb::txn &txn,
+                      const std::set &spaces_with_updates,
+                      std::set rooms_with_updates);
 
-        {
-                constexpr auto type = mtx::events::state_content_to_type;
-                static_assert(type != mtx::events::EventType::Unsupported,
-                              "Not a supported type in state events.");
+    lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
+    {
+        return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
+    }
 
-                if (room_id.empty())
-                        return {};
+    lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
+    }
 
-                std::vector> events;
+    lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+    }
 
-                {
-                        auto db                   = getStatesKeyDb(txn, room_id);
-                        auto eventsDb             = getEventsDb(txn, room_id);
-                        const auto typeStr        = to_string(type);
-                        std::string_view typeStrV = typeStr;
-                        std::string_view data;
-                        std::string_view value;
+    // inverse of EventOrderDb
+    lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
+    }
 
-                        auto cursor = lmdb::cursor::open(txn, db);
-                        bool first  = true;
-                        if (cursor.get(typeStrV, data, MDB_SET)) {
-                                while (cursor.get(
-                                  typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
-                                        first = false;
+    lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
+    }
 
-                                        if (eventsDb.get(txn,
-                                                         json::parse(data)["id"].get(),
-                                                         value))
-                                                events.push_back(
-                                                  json::parse(value)
-                                                    .get>());
-                                }
-                        }
-                }
+    lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+    }
 
-                return events;
-        }
-        void saveInvites(lmdb::txn &txn,
-                         const std::map &rooms);
+    lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
+    }
 
-        void savePresence(
-          lmdb::txn &txn,
-          const std::vector> &presenceUpdates);
+    lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(
+          txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
+    }
 
-        //! Sends signals for the rooms that are removed.
-        void removeLeftRooms(lmdb::txn &txn,
-                             const std::map &rooms)
-        {
-                for (const auto &room : rooms) {
-                        removeRoom(txn, room.first);
+    lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
+    }
 
-                        // Clean up leftover invites.
-                        removeInvite(txn, room.first);
-                }
-        }
+    lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
+    }
 
-        void updateSpaces(lmdb::txn &txn,
-                          const std::set &spaces_with_updates,
-                          std::set rooms_with_updates);
+    lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
+    }
 
-        lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
-        }
+    lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        auto db = lmdb::dbi::open(
+          txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT);
+        lmdb::dbi_set_dupsort(txn, db, compare_state_key);
+        return db;
+    }
 
-        lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
-        }
+    lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
+    }
 
-        lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
-        }
+    lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
+    }
 
-        // inverse of EventOrderDb
-        lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
-        }
+    lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id)
+    {
+        return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE);
+    }
 
-        lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
-        }
+    lmdb::dbi getPresenceDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "presence", MDB_CREATE); }
 
-        lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
-        }
+    lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); }
 
-        lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
-        }
+    lmdb::dbi getVerificationDb(lmdb::txn &txn)
+    {
+        return lmdb::dbi::open(txn, "verified", MDB_CREATE);
+    }
 
-        lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
-        }
+    //! Retrieves or creates the database that stores the open OLM sessions between our device
+    //! and the given curve25519 key which represents another device.
+    //!
+    //! Each entry is a map from the session_id to the pickled representation of the session.
+    lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key)
+    {
+        return lmdb::dbi::open(
+          txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE);
+    }
 
-        lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
-        }
+    QString getDisplayName(const mtx::events::StateEvent &event)
+    {
+        if (!event.content.display_name.empty())
+            return QString::fromStdString(event.content.display_name);
 
-        lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
-        }
+        return QString::fromStdString(event.state_key);
+    }
 
-        lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
-        }
+    std::optional verificationCache(const std::string &user_id, lmdb::txn &txn);
+    VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
+    std::optional userKeys_(const std::string &user_id, lmdb::txn &txn);
 
-        lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                auto db = lmdb::dbi::open(
-                  txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT);
-                lmdb::dbi_set_dupsort(txn, db, compare_state_key);
-                return db;
-        }
+    void setNextBatchToken(lmdb::txn &txn, const std::string &token);
 
-        lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
-        }
+    lmdb::env env_;
+    lmdb::dbi syncStateDb_;
+    lmdb::dbi roomsDb_;
+    lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
+    lmdb::dbi invitesDb_;
+    lmdb::dbi readReceiptsDb_;
+    lmdb::dbi notificationsDb_;
 
-        lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
-        }
+    lmdb::dbi devicesDb_;
+    lmdb::dbi deviceKeysDb_;
 
-        lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id)
-        {
-                return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE);
-        }
+    lmdb::dbi inboundMegolmSessionDb_;
+    lmdb::dbi outboundMegolmSessionDb_;
+    lmdb::dbi megolmSessionDataDb_;
 
-        lmdb::dbi getPresenceDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "presence", MDB_CREATE);
-        }
+    lmdb::dbi encryptedRooms_;
 
-        lmdb::dbi getUserKeysDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
-        }
+    QString localUserId_;
+    QString cacheDirectory_;
 
-        lmdb::dbi getVerificationDb(lmdb::txn &txn)
-        {
-                return lmdb::dbi::open(txn, "verified", MDB_CREATE);
-        }
+    std::string pickle_secret_;
 
-        //! Retrieves or creates the database that stores the open OLM sessions between our device
-        //! and the given curve25519 key which represents another device.
-        //!
-        //! Each entry is a map from the session_id to the pickled representation of the session.
-        lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key)
-        {
-                return lmdb::dbi::open(
-                  txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE);
-        }
+    VerificationStorage verification_storage;
 
-        QString getDisplayName(const mtx::events::StateEvent &event)
-        {
-                if (!event.content.display_name.empty())
-                        return QString::fromStdString(event.content.display_name);
-
-                return QString::fromStdString(event.state_key);
-        }
-
-        std::optional verificationCache(const std::string &user_id,
-                                                           lmdb::txn &txn);
-        VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
-        std::optional userKeys_(const std::string &user_id, lmdb::txn &txn);
-
-        void setNextBatchToken(lmdb::txn &txn, const std::string &token);
-        void setNextBatchToken(lmdb::txn &txn, const QString &token);
-
-        lmdb::env env_;
-        lmdb::dbi syncStateDb_;
-        lmdb::dbi roomsDb_;
-        lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
-        lmdb::dbi invitesDb_;
-        lmdb::dbi readReceiptsDb_;
-        lmdb::dbi notificationsDb_;
-
-        lmdb::dbi devicesDb_;
-        lmdb::dbi deviceKeysDb_;
-
-        lmdb::dbi inboundMegolmSessionDb_;
-        lmdb::dbi outboundMegolmSessionDb_;
-        lmdb::dbi megolmSessionDataDb_;
-
-        lmdb::dbi encryptedRooms_;
-
-        QString localUserId_;
-        QString cacheDirectory_;
-
-        VerificationStorage verification_storage;
-
-        bool databaseReady_ = false;
+    bool databaseReady_ = false;
 };
 
 namespace cache {
diff --git a/src/CallDevices.cpp b/src/CallDevices.cpp
deleted file mode 100644
index 825d2f72..00000000
--- a/src/CallDevices.cpp
+++ /dev/null
@@ -1,396 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-
-#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 frameRates;
-        };
-        std::string name;
-        GstDevice *device;
-        std::vector caps;
-};
-
-std::vector audioSources_;
-std::vector videoSources_;
-
-using FrameRate = std::pair;
-std::optional
-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 &rates, const FrameRate &rate)
-{
-        constexpr double minimumFrameRate = 15.0;
-        if (static_cast(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
-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
-std::vector
-deviceNames(T &sources, const std::string &defaultDevice)
-{
-        std::vector 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
-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
-tokenise(std::string_view str, char delim)
-{
-        std::pair 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
-CallDevices::names(bool isVideo, const std::string &defaultDevice) const
-{
-        return isVideo ? deviceNames(videoSources_, defaultDevice)
-                       : deviceNames(audioSources_, defaultDevice);
-}
-
-std::vector
-CallDevices::resolutions(const std::string &cameraName) const
-{
-        std::vector 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
-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 &resolution, std::pair &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
-CallDevices::names(bool, const std::string &) const
-{
-        return {};
-}
-
-std::vector
-CallDevices::resolutions(const std::string &) const
-{
-        return {};
-}
-
-std::vector
-CallDevices::frameRates(const std::string &, const std::string &) const
-{
-        return {};
-}
-
-#endif
diff --git a/src/CallDevices.h b/src/CallDevices.h
deleted file mode 100644
index 69325f97..00000000
--- a/src/CallDevices.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-#include 
-
-#include 
-
-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 names(bool isVideo, const std::string &defaultDevice) const;
-        std::vector resolutions(const std::string &cameraName) const;
-        std::vector 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 &resolution,
-                               std::pair &frameRate) const;
-
-public:
-        CallDevices(CallDevices const &) = delete;
-        void operator=(CallDevices const &) = delete;
-};
diff --git a/src/CallManager.cpp b/src/CallManager.cpp
deleted file mode 100644
index 6d41f1c6..00000000
--- a/src/CallManager.cpp
+++ /dev/null
@@ -1,689 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-
-#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 
-#include 
-#endif
-
-#ifdef GSTREAMER_AVAILABLE
-extern "C"
-{
-#include "gst/gst.h"
-}
-#endif
-
-Q_DECLARE_METATYPE(std::vector)
-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
-getTurnURIs(const mtx::responses::TurnServer &turnServer);
-}
-
-CallManager::CallManager(QObject *parent)
-  : QObject(parent)
-  , session_(WebRTCSession::instance())
-  , turnServerTimer_(this)
-{
-        qRegisterMetaType>();
-        qRegisterMetaType();
-        qRegisterMetaType();
-
-        connect(
-          &session_,
-          &WebRTCSession::offerCreated,
-          this,
-          [this](const std::string &sdp, const std::vector &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 &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::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 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(event) || handleEvent(event) ||
-            handleEvent(event) || handleEvent(event))
-                return;
-#else
-        (void)event;
-#endif
-}
-
-template
-bool
-CallManager::handleEvent(const mtx::events::collections::TimelineEvents &event)
-{
-        if (std::holds_alternative>(event)) {
-                handleEvent(std::get>(event));
-                return true;
-        }
-        return false;
-}
-
-void
-CallManager::handleEvent(const RoomEvent &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 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 &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 &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 &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 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(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> 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>
-          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
-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 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;
-}
-}
diff --git a/src/CallManager.h b/src/CallManager.h
deleted file mode 100644
index 1d973191..00000000
--- a/src/CallManager.h
+++ /dev/null
@@ -1,115 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-
-#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 remoteICECandidates_;
-        std::vector turnURIs_;
-        QTimer turnServerTimer_;
-        QMediaPlayer player_;
-        std::vector> windows_;
-
-        template
-        bool handleEvent(const mtx::events::collections::TimelineEvents &event);
-        void handleEvent(const mtx::events::RoomEvent &);
-        void handleEvent(const mtx::events::RoomEvent &);
-        void handleEvent(const mtx::events::RoomEvent &);
-        void handleEvent(const mtx::events::RoomEvent &);
-        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();
-};
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 8a0e891b..d262387c 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -6,26 +6,25 @@
 #include 
 #include 
 #include 
-#include 
 
 #include 
 
 #include "AvatarProvider.h"
 #include "Cache.h"
 #include "Cache_p.h"
-#include "CallManager.h"
 #include "ChatPage.h"
-#include "DeviceVerificationFlow.h"
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MainWindow.h"
 #include "MatrixClient.h"
-#include "Olm.h"
 #include "UserSettingsPage.h"
 #include "Utils.h"
+#include "encryption/DeviceVerificationFlow.h"
+#include "encryption/Olm.h"
 #include "ui/OverlayModal.h"
 #include "ui/Theme.h"
 #include "ui/UserProfile.h"
+#include "voip/CallManager.h"
 
 #include "notifications/Manager.h"
 
@@ -33,9 +32,6 @@
 
 #include "blurhash.hpp"
 
-// TODO: Needs to be updated with an actual secret.
-static const std::string STORAGE_SECRET_KEY("secret");
-
 ChatPage *ChatPage::instance_             = nullptr;
 constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
 constexpr int RETRY_TIMEOUT               = 5'000;
@@ -54,643 +50,641 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
   , notificationsManager(this)
   , callManager_(new CallManager(this))
 {
-        setObjectName("chatPage");
+    setObjectName("chatPage");
 
-        instance_ = this;
+    instance_ = this;
 
-        qRegisterMetaType>();
-        qRegisterMetaType>();
-        qRegisterMetaType();
-        qRegisterMetaType();
-        qRegisterMetaType();
+    qRegisterMetaType>();
+    qRegisterMetaType>();
+    qRegisterMetaType();
+    qRegisterMetaType();
+    qRegisterMetaType();
 
-        topLayout_ = new QHBoxLayout(this);
-        topLayout_->setSpacing(0);
-        topLayout_->setMargin(0);
+    topLayout_ = new QHBoxLayout(this);
+    topLayout_->setSpacing(0);
+    topLayout_->setMargin(0);
 
-        view_manager_ = new TimelineViewManager(callManager_, this);
+    view_manager_ = new TimelineViewManager(callManager_, this);
 
-        topLayout_->addWidget(view_manager_->getWidget());
+    topLayout_->addWidget(view_manager_->getWidget());
 
-        connect(this,
-                &ChatPage::downloadedSecrets,
-                this,
-                &ChatPage::decryptDownloadedSecrets,
-                Qt::QueuedConnection);
+    connect(this,
+            &ChatPage::downloadedSecrets,
+            this,
+            &ChatPage::decryptDownloadedSecrets,
+            Qt::QueuedConnection);
 
-        connect(this, &ChatPage::connectionLost, this, [this]() {
-                nhlog::net()->info("connectivity lost");
-                isConnected_ = false;
-                http::client()->shutdown();
-        });
-        connect(this, &ChatPage::connectionRestored, this, [this]() {
-                nhlog::net()->info("trying to re-connect");
-                isConnected_ = true;
+    connect(this, &ChatPage::connectionLost, this, [this]() {
+        nhlog::net()->info("connectivity lost");
+        isConnected_ = false;
+        http::client()->shutdown();
+    });
+    connect(this, &ChatPage::connectionRestored, this, [this]() {
+        nhlog::net()->info("trying to re-connect");
+        isConnected_ = true;
 
-                // Drop all pending connections.
-                http::client()->shutdown();
-                trySync();
-        });
+        // Drop all pending connections.
+        http::client()->shutdown();
+        trySync();
+    });
 
-        connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
-        connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
-                if (http::client()->access_token().empty()) {
-                        connectivityTimer_.stop();
-                        return;
-                }
+    connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
+    connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
+        if (http::client()->access_token().empty()) {
+            connectivityTimer_.stop();
+            return;
+        }
 
-                http::client()->versions(
-                  [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
-                          if (err) {
-                                  emit connectionLost();
-                                  return;
-                          }
+        http::client()->versions(
+          [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+              if (err) {
+                  emit connectionLost();
+                  return;
+              }
 
-                          if (!isConnected_)
-                                  emit connectionRestored();
-                  });
-        });
-
-        connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
-
-        connect(
-          view_manager_,
-          &TimelineViewManager::inviteUsers,
-          this,
-          [this](QString roomId, QStringList users) {
-                  for (int ii = 0; ii < users.size(); ++ii) {
-                          QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
-                                  const auto user = users.at(ii);
-
-                                  http::client()->invite_user(
-                                    roomId.toStdString(),
-                                    user.toStdString(),
-                                    [this, user](const mtx::responses::RoomInvite &,
-                                                 mtx::http::RequestErr err) {
-                                            if (err) {
-                                                    emit showNotification(
-                                                      tr("Failed to invite user: %1").arg(user));
-                                                    return;
-                                            }
-
-                                            emit showNotification(tr("Invited user: %1").arg(user));
-                                    });
-                          });
-                  }
+              if (!isConnected_)
+                  emit connectionRestored();
           });
+    });
 
-        connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
-        connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
-        connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
-        connect(this,
-                &ChatPage::highlightedNotifsRetrieved,
-                this,
-                [](const mtx::responses::Notifications ¬if) {
-                        try {
-                                cache::saveTimelineMentions(notif);
-                        } catch (const lmdb::error &e) {
-                                nhlog::db()->error("failed to save mentions: {}", e.what());
+    connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
+
+    connect(
+      view_manager_,
+      &TimelineViewManager::inviteUsers,
+      this,
+      [this](QString roomId, QStringList users) {
+          for (int ii = 0; ii < users.size(); ++ii) {
+              QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
+                  const auto user = users.at(ii);
+
+                  http::client()->invite_user(
+                    roomId.toStdString(),
+                    user.toStdString(),
+                    [this, user](const mtx::responses::RoomInvite &, mtx::http::RequestErr err) {
+                        if (err) {
+                            emit showNotification(tr("Failed to invite user: %1").arg(user));
+                            return;
                         }
-                });
 
-        connect(¬ificationsManager,
-                &NotificationsManager::notificationClicked,
-                this,
-                [this](const QString &roomid, const QString &eventid) {
-                        Q_UNUSED(eventid)
-                        view_manager_->rooms()->setCurrentRoom(roomid);
-                        activateWindow();
-                });
-        connect(¬ificationsManager,
-                &NotificationsManager::sendNotificationReply,
-                this,
-                [this](const QString &roomid, const QString &eventid, const QString &body) {
-                        view_manager_->rooms()->setCurrentRoom(roomid);
-                        view_manager_->queueReply(roomid, eventid, body);
-                        activateWindow();
-                });
+                        emit showNotification(tr("Invited user: %1").arg(user));
+                    });
+              });
+          }
+      });
 
-        connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
-                // ensure the qml context is shutdown before we destroy all other singletons
-                // Otherwise Qml will try to access the room list or settings, after they have been
-                // destroyed
-                topLayout_->removeWidget(view_manager_->getWidget());
-                delete view_manager_->getWidget();
-        });
-
-        connect(
-          this,
-          &ChatPage::initializeViews,
-          view_manager_,
-          [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); },
-          Qt::QueuedConnection);
-        connect(this,
-                &ChatPage::initializeEmptyViews,
-                view_manager_,
-                &TimelineViewManager::initializeRoomlist);
-        connect(
-          this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
-        connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
-                view_manager_->sync(rooms);
-
-                bool hasNotifications = false;
-                for (const auto &room : rooms.join) {
-                        if (room.second.unread_notifications.notification_count > 0)
-                                hasNotifications = true;
+    connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
+    connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
+    connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
+    connect(this,
+            &ChatPage::highlightedNotifsRetrieved,
+            this,
+            [](const mtx::responses::Notifications ¬if) {
+                try {
+                    cache::saveTimelineMentions(notif);
+                } catch (const lmdb::error &e) {
+                    nhlog::db()->error("failed to save mentions: {}", e.what());
                 }
+            });
 
-                if (hasNotifications && userSettings_->hasNotifications())
-                        http::client()->notifications(
-                          5,
-                          "",
-                          "",
-                          [this](const mtx::responses::Notifications &res,
-                                 mtx::http::RequestErr err) {
-                                  if (err) {
-                                          nhlog::net()->warn(
-                                            "failed to retrieve notifications: {} ({})",
-                                            err->matrix_error.error,
-                                            static_cast(err->status_code));
-                                          return;
-                                  }
+    connect(¬ificationsManager,
+            &NotificationsManager::notificationClicked,
+            this,
+            [this](const QString &roomid, const QString &eventid) {
+                Q_UNUSED(eventid)
+                view_manager_->rooms()->setCurrentRoom(roomid);
+                activateWindow();
+            });
+    connect(¬ificationsManager,
+            &NotificationsManager::sendNotificationReply,
+            this,
+            [this](const QString &roomid, const QString &eventid, const QString &body) {
+                view_manager_->rooms()->setCurrentRoom(roomid);
+                view_manager_->queueReply(roomid, eventid, body);
+                activateWindow();
+            });
 
-                                  emit notificationsRetrieved(std::move(res));
-                          });
-        });
+    connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
+        // ensure the qml context is shutdown before we destroy all other singletons
+        // Otherwise Qml will try to access the room list or settings, after they have been
+        // destroyed
+        topLayout_->removeWidget(view_manager_->getWidget());
+        delete view_manager_->getWidget();
+    });
 
-        connect(
-          this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
-        connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection);
-        connect(
-          this,
-          &ChatPage::tryDelayedSyncCb,
-          this,
-          [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
-          Qt::QueuedConnection);
+    connect(
+      this,
+      &ChatPage::initializeViews,
+      view_manager_,
+      [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); },
+      Qt::QueuedConnection);
+    connect(this,
+            &ChatPage::initializeEmptyViews,
+            view_manager_,
+            &TimelineViewManager::initializeRoomlist);
+    connect(
+      this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
+    connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
+        view_manager_->sync(rooms);
 
-        connect(this,
-                &ChatPage::newSyncResponse,
-                this,
-                &ChatPage::handleSyncResponse,
-                Qt::QueuedConnection);
+        static unsigned int prevNotificationCount = 0;
+        unsigned int notificationCount            = 0;
+        for (const auto &room : rooms.join) {
+            notificationCount += room.second.unread_notifications.notification_count;
+        }
 
-        connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
+        // HACK: If we had less notifications last time we checked, send an alert if the
+        // user wanted one. Technically, this may cause an alert to be missed if new ones
+        // come in while you are reading old ones. Since the window is almost certainly open
+        // in this edge case, that's probably a non-issue.
+        // TODO: Replace this once we have proper pushrules support. This is a horrible hack
+        if (prevNotificationCount < notificationCount) {
+            if (userSettings_->hasAlertOnNotification())
+                QApplication::alert(this);
+        }
+        prevNotificationCount = notificationCount;
 
-        connectCallMessage();
-        connectCallMessage();
-        connectCallMessage();
-        connectCallMessage();
+        // No need to check amounts for this section, as this function internally checks for
+        // duplicates.
+        if (notificationCount && userSettings_->hasNotifications())
+            http::client()->notifications(
+              5,
+              "",
+              "",
+              [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) {
+                  if (err) {
+                      nhlog::net()->warn("failed to retrieve notifications: {} ({})",
+                                         err->matrix_error.error,
+                                         static_cast(err->status_code));
+                      return;
+                  }
+
+                  emit notificationsRetrieved(std::move(res));
+              });
+    });
+
+    connect(
+      this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
+    connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection);
+    connect(
+      this,
+      &ChatPage::tryDelayedSyncCb,
+      this,
+      [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); },
+      Qt::QueuedConnection);
+
+    connect(
+      this, &ChatPage::newSyncResponse, this, &ChatPage::handleSyncResponse, Qt::QueuedConnection);
+
+    connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
+
+    connectCallMessage();
+    connectCallMessage();
+    connectCallMessage();
+    connectCallMessage();
 }
 
 void
 ChatPage::logout()
 {
-        resetUI();
-        deleteConfigs();
+    resetUI();
+    deleteConfigs();
 
-        emit closing();
-        connectivityTimer_.stop();
+    emit closing();
+    connectivityTimer_.stop();
 }
 
 void
 ChatPage::dropToLoginPage(const QString &msg)
 {
-        nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
+    nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
 
-        http::client()->shutdown();
-        connectivityTimer_.stop();
+    http::client()->shutdown();
+    connectivityTimer_.stop();
 
-        resetUI();
-        deleteConfigs();
+    resetUI();
+    deleteConfigs();
 
-        emit showLoginPage(msg);
+    emit showLoginPage(msg);
 }
 
 void
 ChatPage::resetUI()
 {
-        view_manager_->clearAll();
+    view_manager_->clearAll();
 
-        emit unreadMessages(0);
+    emit unreadMessages(0);
 }
 
 void
 ChatPage::deleteConfigs()
 {
-        QSettings settings;
+    auto settings = UserSettings::instance()->qsettings();
 
-        if (UserSettings::instance()->profile() != "") {
-                settings.beginGroup("profile");
-                settings.beginGroup(UserSettings::instance()->profile());
-        }
-        settings.beginGroup("auth");
-        settings.remove("");
-        settings.endGroup(); // auth
+    if (UserSettings::instance()->profile() != "") {
+        settings->beginGroup("profile");
+        settings->beginGroup(UserSettings::instance()->profile());
+    }
+    settings->beginGroup("auth");
+    settings->remove("");
+    settings->endGroup(); // auth
 
-        http::client()->shutdown();
-        cache::deleteData();
+    http::client()->shutdown();
+    cache::deleteData();
 }
 
 void
 ChatPage::bootstrap(QString userid, QString homeserver, QString token)
 {
-        using namespace mtx::identifiers;
+    using namespace mtx::identifiers;
 
-        try {
-                http::client()->set_user(parse(userid.toStdString()));
-        } catch (const std::invalid_argument &) {
-                nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
-                                      userid.toStdString());
-        }
+    try {
+        http::client()->set_user(parse(userid.toStdString()));
+    } catch (const std::invalid_argument &) {
+        nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString());
+    }
 
-        http::client()->set_server(homeserver.toStdString());
-        http::client()->set_access_token(token.toStdString());
-        http::client()->verify_certificates(
-          !UserSettings::instance()->disableCertificateValidation());
+    http::client()->set_server(homeserver.toStdString());
+    http::client()->set_access_token(token.toStdString());
+    http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
 
-        // The Olm client needs the user_id & device_id that will be included
-        // in the generated payloads & keys.
-        olm::client()->set_user_id(http::client()->user_id().to_string());
-        olm::client()->set_device_id(http::client()->device_id());
+    // The Olm client needs the user_id & device_id that will be included
+    // in the generated payloads & keys.
+    olm::client()->set_user_id(http::client()->user_id().to_string());
+    olm::client()->set_device_id(http::client()->device_id());
 
-        try {
-                cache::init(userid);
+    try {
+        cache::init(userid);
 
-                connect(cache::client(),
-                        &Cache::newReadReceipts,
-                        view_manager_,
-                        &TimelineViewManager::updateReadReceipts);
+        connect(cache::client(),
+                &Cache::newReadReceipts,
+                view_manager_,
+                &TimelineViewManager::updateReadReceipts);
 
-                connect(cache::client(),
-                        &Cache::removeNotification,
-                        ¬ificationsManager,
-                        &NotificationsManager::removeNotification);
+        connect(cache::client(),
+                &Cache::removeNotification,
+                ¬ificationsManager,
+                &NotificationsManager::removeNotification);
 
-                const bool isInitialized = cache::isInitialized();
-                const auto cacheVersion  = cache::formatVersion();
+        const bool isInitialized = cache::isInitialized();
+        const auto cacheVersion  = cache::formatVersion();
 
-                callManager_->refreshTurnServer();
+        callManager_->refreshTurnServer();
 
-                if (!isInitialized) {
-                        cache::setCurrentFormat();
-                } else {
-                        if (cacheVersion == cache::CacheVersion::Current) {
-                                loadStateFromCache();
-                                return;
-                        } else if (cacheVersion == cache::CacheVersion::Older) {
-                                if (!cache::runMigrations()) {
-                                        QMessageBox::critical(
-                                          this,
+        if (!isInitialized) {
+            cache::setCurrentFormat();
+        } else {
+            if (cacheVersion == cache::CacheVersion::Current) {
+                loadStateFromCache();
+                return;
+            } else if (cacheVersion == cache::CacheVersion::Older) {
+                if (!cache::runMigrations()) {
+                    QMessageBox::critical(this,
                                           tr("Cache migration failed!"),
                                           tr("Migrating the cache to the current version failed. "
                                              "This can have different reasons. Please open an "
                                              "issue and try to use an older version in the mean "
                                              "time. Alternatively you can try deleting the cache "
                                              "manually."));
-                                        QCoreApplication::quit();
-                                }
-                                loadStateFromCache();
-                                return;
-                        } else if (cacheVersion == cache::CacheVersion::Newer) {
-                                QMessageBox::critical(
-                                  this,
-                                  tr("Incompatible cache version"),
-                                  tr("The cache on your disk is newer than this version of Nheko "
-                                     "supports. Please update or clear your cache."));
-                                QCoreApplication::quit();
-                                return;
-                        }
+                    QCoreApplication::quit();
                 }
-
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failure during boot: {}", e.what());
-                cache::deleteData();
-                nhlog::net()->info("falling back to initial sync");
+                loadStateFromCache();
+                return;
+            } else if (cacheVersion == cache::CacheVersion::Newer) {
+                QMessageBox::critical(
+                  this,
+                  tr("Incompatible cache version"),
+                  tr("The cache on your disk is newer than this version of Nheko "
+                     "supports. Please update or clear your cache."));
+                QCoreApplication::quit();
+                return;
+            }
         }
 
-        try {
-                // It's the first time syncing with this device
-                // There isn't a saved olm account to restore.
-                nhlog::crypto()->info("creating new olm account");
-                olm::client()->create_new_account();
-                cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY));
-        } catch (const lmdb::error &e) {
-                nhlog::crypto()->critical("failed to save olm account {}", e.what());
-                emit dropToLoginPageCb(QString::fromStdString(e.what()));
-                return;
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to create new olm account {}", e.what());
-                emit dropToLoginPageCb(QString::fromStdString(e.what()));
-                return;
-        }
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("failure during boot: {}", e.what());
+        cache::deleteData();
+        nhlog::net()->info("falling back to initial sync");
+    }
 
-        getProfileInfo();
-        tryInitialSync();
+    try {
+        // It's the first time syncing with this device
+        // There isn't a saved olm account to restore.
+        nhlog::crypto()->info("creating new olm account");
+        olm::client()->create_new_account();
+        cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret()));
+    } catch (const lmdb::error &e) {
+        nhlog::crypto()->critical("failed to save olm account {}", e.what());
+        emit dropToLoginPageCb(QString::fromStdString(e.what()));
+        return;
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to create new olm account {}", e.what());
+        emit dropToLoginPageCb(QString::fromStdString(e.what()));
+        return;
+    }
+
+    getProfileInfo();
+    getBackupVersion();
+    tryInitialSync();
 }
 
 void
 ChatPage::loadStateFromCache()
 {
-        nhlog::db()->info("restoring state from cache");
+    nhlog::db()->info("restoring state from cache");
 
-        try {
-                olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
+    try {
+        olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret());
 
-                emit initializeEmptyViews();
-                emit initializeMentions(cache::getTimelineMentions());
+        emit initializeEmptyViews();
+        emit initializeMentions(cache::getTimelineMentions());
 
-                cache::calculateRoomReadStatus();
+        cache::calculateRoomReadStatus();
 
-        } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
-                emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again."));
-                return;
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failed to restore cache: {}", e.what());
-                emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
-                return;
-        } catch (const json::exception &e) {
-                nhlog::db()->critical("failed to parse cache data: {}", e.what());
-                return;
-        }
+    } catch (const mtx::crypto::olm_exception &e) {
+        nhlog::crypto()->critical("failed to restore olm account: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again."));
+        return;
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("failed to restore cache: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
+        return;
+    } catch (const json::exception &e) {
+        nhlog::db()->critical("failed to parse cache data: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
+        return;
+    } catch (const std::exception &e) {
+        nhlog::db()->critical("failed to load cache data: {}", e.what());
+        emit dropToLoginPageCb(tr("Failed to restore save data. Please login again."));
+        return;
+    }
 
-        nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
-        nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+    nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+    nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
 
-        getProfileInfo();
+    getProfileInfo();
+    getBackupVersion();
+    verifyOneTimeKeyCountAfterStartup();
 
-        emit contentLoaded();
+    emit contentLoaded();
 
-        // Start receiving events.
-        emit trySyncCb();
+    // Start receiving events.
+    emit trySyncCb();
 }
 
 void
 ChatPage::removeRoom(const QString &room_id)
 {
-        try {
-                cache::removeRoom(room_id);
-                cache::removeInvite(room_id.toStdString());
-        } catch (const lmdb::error &e) {
-                nhlog::db()->critical("failure while removing room: {}", e.what());
-                // TODO: Notify the user.
-        }
+    try {
+        cache::removeRoom(room_id);
+        cache::removeInvite(room_id.toStdString());
+    } catch (const lmdb::error &e) {
+        nhlog::db()->critical("failure while removing room: {}", e.what());
+        // TODO: Notify the user.
+    }
 }
 
 void
 ChatPage::sendNotifications(const mtx::responses::Notifications &res)
 {
-        for (const auto &item : res.notifications) {
-                const auto event_id = mtx::accessors::event_id(item.event);
+    for (const auto &item : res.notifications) {
+        const auto event_id = mtx::accessors::event_id(item.event);
 
-                try {
-                        if (item.read) {
-                                cache::removeReadNotification(event_id);
-                                continue;
-                        }
+        try {
+            if (item.read) {
+                cache::removeReadNotification(event_id);
+                continue;
+            }
 
-                        if (!cache::isNotificationSent(event_id)) {
-                                const auto room_id = QString::fromStdString(item.room_id);
+            if (!cache::isNotificationSent(event_id)) {
+                const auto room_id = QString::fromStdString(item.room_id);
 
-                                // We should only sent one notification per event.
-                                cache::markSentNotification(event_id);
+                // We should only sent one notification per event.
+                cache::markSentNotification(event_id);
 
-                                // Don't send a notification when the current room is opened.
-                                if (isRoomActive(room_id))
-                                        continue;
+                // Don't send a notification when the current room is opened.
+                if (isRoomActive(room_id))
+                    continue;
 
-                                if (userSettings_->hasAlertOnNotification()) {
-                                        QApplication::alert(this);
-                                }
+                if (userSettings_->hasDesktopNotifications()) {
+                    auto info = cache::singleRoomInfo(item.room_id);
 
-                                if (userSettings_->hasDesktopNotifications()) {
-                                        auto info = cache::singleRoomInfo(item.room_id);
-
-                                        AvatarProvider::resolve(
-                                          QString::fromStdString(info.avatar_url),
-                                          96,
-                                          this,
-                                          [this, item](QPixmap image) {
-                                                  notificationsManager.postNotification(
-                                                    item, image.toImage());
-                                          });
-                                }
-                        }
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->warn("error while sending notification: {}", e.what());
+                    AvatarProvider::resolve(QString::fromStdString(info.avatar_url),
+                                            96,
+                                            this,
+                                            [this, item](QPixmap image) {
+                                                notificationsManager.postNotification(
+                                                  item, image.toImage());
+                                            });
                 }
+            }
+        } catch (const lmdb::error &e) {
+            nhlog::db()->warn("error while sending notification: {}", e.what());
         }
+    }
 }
 
 void
 ChatPage::tryInitialSync()
 {
-        nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
-        nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+    nhlog::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+    nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
 
-        // Upload one time keys for the device.
-        nhlog::crypto()->info("generating one time keys");
-        olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS);
+    // Upload one time keys for the device.
+    nhlog::crypto()->info("generating one time keys");
+    olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS);
 
-        http::client()->upload_keys(
-          olm::client()->create_upload_keys_request(),
-          [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          const int status_code = static_cast(err->status_code);
+    http::client()->upload_keys(
+      olm::client()->create_upload_keys_request(),
+      [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
+          if (err) {
+              const int status_code = static_cast(err->status_code);
 
-                          if (status_code == 404) {
-                                  nhlog::net()->warn(
-                                    "skipping key uploading. server doesn't provide /keys/upload");
-                                  return startInitialSync();
-                          }
+              if (status_code == 404) {
+                  nhlog::net()->warn("skipping key uploading. server doesn't provide /keys/upload");
+                  return startInitialSync();
+              }
 
-                          nhlog::crypto()->critical("failed to upload one time keys: {} {}",
-                                                    err->matrix_error.error,
-                                                    status_code);
+              nhlog::crypto()->critical(
+                "failed to upload one time keys: {} {}", err->matrix_error.error, status_code);
 
-                          QString errorMsg(tr("Failed to setup encryption keys. Server response: "
-                                              "%1 %2. Please try again later.")
-                                             .arg(QString::fromStdString(err->matrix_error.error))
-                                             .arg(status_code));
+              QString errorMsg(tr("Failed to setup encryption keys. Server response: "
+                                  "%1 %2. Please try again later.")
+                                 .arg(QString::fromStdString(err->matrix_error.error))
+                                 .arg(status_code));
 
-                          emit dropToLoginPageCb(errorMsg);
-                          return;
-                  }
+              emit dropToLoginPageCb(errorMsg);
+              return;
+          }
 
-                  olm::mark_keys_as_published();
+          olm::mark_keys_as_published();
 
-                  for (const auto &entry : res.one_time_key_counts)
-                          nhlog::net()->info(
-                            "uploaded {} {} one-time keys", entry.second, entry.first);
+          for (const auto &entry : res.one_time_key_counts)
+              nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first);
 
-                  startInitialSync();
-          });
+          startInitialSync();
+      });
 }
 
 void
 ChatPage::startInitialSync()
 {
-        nhlog::net()->info("trying initial sync");
+    nhlog::net()->info("trying initial sync");
 
-        mtx::http::SyncOpts opts;
-        opts.timeout      = 0;
-        opts.set_presence = currentPresence();
+    mtx::http::SyncOpts opts;
+    opts.timeout      = 0;
+    opts.set_presence = currentPresence();
 
-        http::client()->sync(
-          opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
-                  // TODO: Initial Sync should include mentions as well...
+    http::client()->sync(opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+        // TODO: Initial Sync should include mentions as well...
 
-                  if (err) {
-                          const auto error      = QString::fromStdString(err->matrix_error.error);
-                          const auto msg        = tr("Please try to login again: %1").arg(error);
-                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
-                          const int status_code = static_cast(err->status_code);
+        if (err) {
+            const auto error      = QString::fromStdString(err->matrix_error.error);
+            const auto msg        = tr("Please try to login again: %1").arg(error);
+            const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+            const int status_code = static_cast(err->status_code);
 
-                          nhlog::net()->error("initial sync error: {} {} {} {}",
-                                              err->parse_error,
-                                              status_code,
-                                              err->error_code,
-                                              err_code);
+            nhlog::net()->error("initial sync error: {} {} {} {}",
+                                err->parse_error,
+                                status_code,
+                                err->error_code,
+                                err_code);
 
-                          // non http related errors
-                          if (status_code <= 0 || status_code >= 600) {
-                                  startInitialSync();
-                                  return;
-                          }
+            // non http related errors
+            if (status_code <= 0 || status_code >= 600) {
+                startInitialSync();
+                return;
+            }
 
-                          switch (status_code) {
-                          case 502:
-                          case 504:
-                          case 524: {
-                                  startInitialSync();
-                                  return;
-                          }
-                          default: {
-                                  emit dropToLoginPageCb(msg);
-                                  return;
-                          }
-                          }
-                  }
+            switch (status_code) {
+            case 502:
+            case 504:
+            case 524: {
+                startInitialSync();
+                return;
+            }
+            default: {
+                emit dropToLoginPageCb(msg);
+                return;
+            }
+            }
+        }
 
-                  nhlog::net()->info("initial sync completed");
+        nhlog::net()->info("initial sync completed");
 
-                  try {
-                          cache::client()->saveState(res);
+        try {
+            cache::client()->saveState(res);
 
-                          olm::handle_to_device_messages(res.to_device.events);
+            olm::handle_to_device_messages(res.to_device.events);
 
-                          emit initializeViews(std::move(res.rooms));
-                          emit initializeMentions(cache::getTimelineMentions());
+            emit initializeViews(std::move(res.rooms));
+            emit initializeMentions(cache::getTimelineMentions());
 
-                          cache::calculateRoomReadStatus();
-                  } catch (const lmdb::error &e) {
-                          nhlog::db()->error("failed to save state after initial sync: {}",
-                                             e.what());
-                          startInitialSync();
-                          return;
-                  }
+            cache::calculateRoomReadStatus();
+        } catch (const lmdb::error &e) {
+            nhlog::db()->error("failed to save state after initial sync: {}", e.what());
+            startInitialSync();
+            return;
+        }
 
-                  emit trySyncCb();
-                  emit contentLoaded();
-          });
+        emit trySyncCb();
+        emit contentLoaded();
+    });
 }
 
 void
 ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token)
 {
-        try {
-                if (prev_batch_token != cache::nextBatchToken()) {
-                        nhlog::net()->warn("Duplicate sync, dropping");
-                        return;
-                }
-        } catch (const lmdb::error &e) {
-                nhlog::db()->warn("Logged out in the mean time, dropping sync");
+    try {
+        if (prev_batch_token != cache::nextBatchToken()) {
+            nhlog::net()->warn("Duplicate sync, dropping");
+            return;
         }
+    } catch (const lmdb::error &e) {
+        nhlog::db()->warn("Logged out in the mean time, dropping sync");
+    }
 
-        nhlog::net()->debug("sync completed: {}", res.next_batch);
+    nhlog::net()->debug("sync completed: {}", res.next_batch);
 
-        // Ensure that we have enough one-time keys available.
-        ensureOneTimeKeyCount(res.device_one_time_keys_count);
+    // Ensure that we have enough one-time keys available.
+    ensureOneTimeKeyCount(res.device_one_time_keys_count);
 
-        // TODO: fine grained error handling
-        try {
-                cache::client()->saveState(res);
-                olm::handle_to_device_messages(res.to_device.events);
+    // TODO: fine grained error handling
+    try {
+        cache::client()->saveState(res);
+        olm::handle_to_device_messages(res.to_device.events);
 
-                auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
+        auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
 
-                emit syncUI(res.rooms);
+        emit syncUI(res.rooms);
 
-                // if we process a lot of syncs (1 every 200ms), this means we clean the
-                // db every 100s
-                static int syncCounter = 0;
-                if (syncCounter++ >= 500) {
-                        cache::deleteOldData();
-                        syncCounter = 0;
-                }
-        } catch (const lmdb::map_full_error &e) {
-                nhlog::db()->error("lmdb is full: {}", e.what());
-                cache::deleteOldData();
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("saving sync response: {}", e.what());
+        // if we process a lot of syncs (1 every 200ms), this means we clean the
+        // db every 100s
+        static int syncCounter = 0;
+        if (syncCounter++ >= 500) {
+            cache::deleteOldData();
+            syncCounter = 0;
         }
+    } catch (const lmdb::map_full_error &e) {
+        nhlog::db()->error("lmdb is full: {}", e.what());
+        cache::deleteOldData();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("saving sync response: {}", e.what());
+    }
 
-        emit trySyncCb();
+    emit trySyncCb();
 }
 
 void
 ChatPage::trySync()
 {
-        mtx::http::SyncOpts opts;
-        opts.set_presence = currentPresence();
+    mtx::http::SyncOpts opts;
+    opts.set_presence = currentPresence();
 
-        if (!connectivityTimer_.isActive())
-                connectivityTimer_.start();
+    if (!connectivityTimer_.isActive())
+        connectivityTimer_.start();
 
-        try {
-                opts.since = cache::nextBatchToken();
-        } catch (const lmdb::error &e) {
-                nhlog::db()->error("failed to retrieve next batch token: {}", e.what());
-                return;
-        }
+    try {
+        opts.since = cache::nextBatchToken();
+    } catch (const lmdb::error &e) {
+        nhlog::db()->error("failed to retrieve next batch token: {}", e.what());
+        return;
+    }
 
-        http::client()->sync(
-          opts,
-          [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          const auto error      = QString::fromStdString(err->matrix_error.error);
-                          const auto msg        = tr("Please try to login again: %1").arg(error);
-                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
-                          const int status_code = static_cast(err->status_code);
+    http::client()->sync(
+      opts, [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+          if (err) {
+              const auto error      = QString::fromStdString(err->matrix_error.error);
+              const auto msg        = tr("Please try to login again: %1").arg(error);
+              const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+              const int status_code = static_cast(err->status_code);
 
-                          if ((http::is_logged_in() &&
-                               (err->matrix_error.errcode ==
-                                  mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
-                                err->matrix_error.errcode ==
-                                  mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
-                              !http::is_logged_in()) {
-                                  emit dropToLoginPageCb(msg);
-                                  return;
-                          }
+              if ((http::is_logged_in() &&
+                   (err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
+                    err->matrix_error.errcode == mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
+                  !http::is_logged_in()) {
+                  emit dropToLoginPageCb(msg);
+                  return;
+              }
 
-                          nhlog::net()->error("sync error: {} {} {} {}",
-                                              err->parse_error,
-                                              status_code,
-                                              err->error_code,
-                                              err_code);
-                          emit tryDelayedSyncCb();
-                          return;
-                  }
+              nhlog::net()->error("sync error: {} {} {} {}",
+                                  err->parse_error,
+                                  status_code,
+                                  err->error_code,
+                                  err_code);
+              emit tryDelayedSyncCb();
+              return;
+          }
 
-                  emit newSyncResponse(res, since);
-          });
+          emit newSyncResponse(res, since);
+      });
 }
 
 void
 ChatPage::joinRoom(const QString &room)
 {
-        const auto room_id = room.toStdString();
-        joinRoomVia(room_id, {}, false);
+    const auto room_id = room.toStdString();
+    joinRoomVia(room_id, {}, false);
 }
 
 void
@@ -698,525 +692,722 @@ ChatPage::joinRoomVia(const std::string &room_id,
                       const std::vector &via,
                       bool promptForConfirmation)
 {
-        if (promptForConfirmation &&
-            QMessageBox::Yes !=
-              QMessageBox::question(
-                this,
-                tr("Confirm join"),
-                tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
-                return;
+    if (promptForConfirmation &&
+        QMessageBox::Yes !=
+          QMessageBox::question(
+            this,
+            tr("Confirm join"),
+            tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
+        return;
 
-        http::client()->join_room(
-          room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to join room: %1")
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                          return;
-                  }
+    http::client()->join_room(
+      room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(
+                tr("Failed to join room: %1").arg(QString::fromStdString(err->matrix_error.error)));
+              return;
+          }
 
-                  emit tr("You joined the room");
+          emit tr("You joined the room");
 
-                  // We remove any invites with the same room_id.
-                  try {
-                          cache::removeInvite(room_id);
-                  } catch (const lmdb::error &e) {
-                          emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
-                  }
+          // We remove any invites with the same room_id.
+          try {
+              cache::removeInvite(room_id);
+          } catch (const lmdb::error &e) {
+              emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
+          }
 
-                  view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
-          });
+          view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
+      });
 }
 
 void
 ChatPage::createRoom(const mtx::requests::CreateRoom &req)
 {
-        http::client()->create_room(
-          req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
-                          const auto error      = err->matrix_error.error;
-                          const int status_code = static_cast(err->status_code);
+    http::client()->create_room(
+      req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
+          if (err) {
+              const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+              const auto error      = err->matrix_error.error;
+              const int status_code = static_cast(err->status_code);
 
-                          nhlog::net()->warn(
-                            "failed to create room: {} {} ({})", error, err_code, status_code);
+              nhlog::net()->warn("failed to create room: {} {} ({})", error, err_code, status_code);
 
-                          emit showNotification(
-                            tr("Room creation failed: %1").arg(QString::fromStdString(error)));
-                          return;
-                  }
+              emit showNotification(
+                tr("Room creation failed: %1").arg(QString::fromStdString(error)));
+              return;
+          }
 
-                  QString newRoomId = QString::fromStdString(res.room_id.to_string());
-                  emit showNotification(tr("Room %1 created.").arg(newRoomId));
-                  emit newRoom(newRoomId);
-          });
+          QString newRoomId = QString::fromStdString(res.room_id.to_string());
+          emit showNotification(tr("Room %1 created.").arg(newRoomId));
+          emit newRoom(newRoomId);
+          emit changeToRoom(newRoomId);
+      });
 }
 
 void
 ChatPage::leaveRoom(const QString &room_id)
 {
-        http::client()->leave_room(
-          room_id.toStdString(),
-          [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to leave room: %1")
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                          return;
-                  }
+    http::client()->leave_room(
+      room_id.toStdString(),
+      [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to leave room: %1")
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+              return;
+          }
 
-                  emit leftRoom(room_id);
-          });
+          emit leftRoom(room_id);
+      });
 }
 
 void
 ChatPage::changeRoom(const QString &room_id)
 {
-        view_manager_->rooms()->setCurrentRoom(room_id);
+    view_manager_->rooms()->setCurrentRoom(room_id);
 }
 
 void
 ChatPage::inviteUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
+    auto room = currentRoom();
 
-        if (QMessageBox::question(this,
-                                  tr("Confirm invite"),
-                                  tr("Do you really want to invite %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
+    if (QMessageBox::question(this,
+                              tr("Confirm invite"),
+                              tr("Do you really want to invite %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
 
-        http::client()->invite_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to invite %1 to %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Invited user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    http::client()->invite_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to invite %1 to %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Invited user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 void
 ChatPage::kickUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
+    auto room = currentRoom();
 
-        if (QMessageBox::question(this,
-                                  tr("Confirm kick"),
-                                  tr("Do you really want to kick %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
+    if (QMessageBox::question(this,
+                              tr("Confirm kick"),
+                              tr("Do you really want to kick %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
 
-        http::client()->kick_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to kick %1 from %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Kicked user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    http::client()->kick_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to kick %1 from %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Kicked user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 void
 ChatPage::banUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
+    auto room = currentRoom();
 
-        if (QMessageBox::question(this,
-                                  tr("Confirm ban"),
-                                  tr("Do you really want to ban %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
+    if (QMessageBox::question(this,
+                              tr("Confirm ban"),
+                              tr("Do you really want to ban %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
 
-        http::client()->ban_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to ban %1 in %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Banned user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    http::client()->ban_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to ban %1 in %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Banned user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 void
 ChatPage::unbanUser(QString userid, QString reason)
 {
-        auto room = currentRoom();
+    auto room = currentRoom();
 
-        if (QMessageBox::question(this,
-                                  tr("Confirm unban"),
-                                  tr("Do you really want to unban %1 (%2)?")
-                                    .arg(cache::displayName(room, userid))
-                                    .arg(userid)) != QMessageBox::Yes)
-                return;
+    if (QMessageBox::question(this,
+                              tr("Confirm unban"),
+                              tr("Do you really want to unban %1 (%2)?")
+                                .arg(cache::displayName(room, userid))
+                                .arg(userid)) != QMessageBox::Yes)
+        return;
 
-        http::client()->unban_user(
-          room.toStdString(),
-          userid.toStdString(),
-          [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
-                  if (err) {
-                          emit showNotification(
-                            tr("Failed to unban %1 in %2: %3")
-                              .arg(userid)
-                              .arg(room)
-                              .arg(QString::fromStdString(err->matrix_error.error)));
-                  } else
-                          emit showNotification(tr("Unbanned user: %1").arg(userid));
-          },
-          reason.trimmed().toStdString());
+    http::client()->unban_user(
+      room.toStdString(),
+      userid.toStdString(),
+      [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
+          if (err) {
+              emit showNotification(tr("Failed to unban %1 in %2: %3")
+                                      .arg(userid)
+                                      .arg(room)
+                                      .arg(QString::fromStdString(err->matrix_error.error)));
+          } else
+              emit showNotification(tr("Unbanned user: %1").arg(userid));
+      },
+      reason.trimmed().toStdString());
 }
 
 void
 ChatPage::receivedSessionKey(const std::string &room_id, const std::string &session_id)
 {
-        view_manager_->receivedSessionKey(room_id, session_id);
+    view_manager_->receivedSessionKey(room_id, session_id);
 }
 
 QString
 ChatPage::status() const
 {
-        return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString()));
+    return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString()));
 }
 
 void
 ChatPage::setStatus(const QString &status)
 {
-        http::client()->put_presence_status(
-          currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to set presence status_msg: {}",
-                                             err->matrix_error.error);
-                  }
-          });
+    http::client()->put_presence_status(
+      currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to set presence status_msg: {}", err->matrix_error.error);
+          }
+      });
 }
 
 mtx::presence::PresenceState
 ChatPage::currentPresence() const
 {
-        switch (userSettings_->presence()) {
-        case UserSettings::Presence::Online:
-                return mtx::presence::online;
-        case UserSettings::Presence::Unavailable:
-                return mtx::presence::unavailable;
-        case UserSettings::Presence::Offline:
-                return mtx::presence::offline;
-        default:
-                return mtx::presence::online;
-        }
+    switch (userSettings_->presence()) {
+    case UserSettings::Presence::Online:
+        return mtx::presence::online;
+    case UserSettings::Presence::Unavailable:
+        return mtx::presence::unavailable;
+    case UserSettings::Presence::Offline:
+        return mtx::presence::offline;
+    default:
+        return mtx::presence::online;
+    }
+}
+
+void
+ChatPage::verifyOneTimeKeyCountAfterStartup()
+{
+    http::client()->upload_keys(
+      olm::client()->create_upload_keys_request(),
+      [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
+                                    err->matrix_error.error,
+                                    static_cast(err->status_code),
+                                    static_cast(err->error_code));
+
+              if (err->status_code < 400 || err->status_code >= 500)
+                  return;
+          }
+
+          std::map key_counts;
+          auto count = 0;
+          if (auto c = res.one_time_key_counts.find(mtx::crypto::SIGNED_CURVE25519);
+              c == res.one_time_key_counts.end()) {
+              key_counts[mtx::crypto::SIGNED_CURVE25519] = 0;
+          } else {
+              key_counts[mtx::crypto::SIGNED_CURVE25519] = c->second;
+              count                                      = c->second;
+          }
+
+          nhlog::crypto()->info(
+            "Fetched server key count {} {}", count, mtx::crypto::SIGNED_CURVE25519);
+
+          ensureOneTimeKeyCount(key_counts);
+      });
 }
 
 void
 ChatPage::ensureOneTimeKeyCount(const std::map &counts)
 {
-        uint16_t count = 0;
-        if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end())
-                count = c->second;
+    if (auto count = counts.find(mtx::crypto::SIGNED_CURVE25519); count != counts.end()) {
+        nhlog::crypto()->debug(
+          "Updated server key count {} {}", count->second, mtx::crypto::SIGNED_CURVE25519);
 
-        if (count < MAX_ONETIME_KEYS) {
-                const int nkeys = MAX_ONETIME_KEYS - count;
+        if (count->second < MAX_ONETIME_KEYS) {
+            const int nkeys = MAX_ONETIME_KEYS - count->second;
 
-                nhlog::crypto()->info(
-                  "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
-                olm::client()->generate_one_time_keys(nkeys);
+            nhlog::crypto()->info("uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
+            olm::client()->generate_one_time_keys(nkeys);
 
-                http::client()->upload_keys(
-                  olm::client()->create_upload_keys_request(),
-                  [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
-                          if (err) {
-                                  nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
-                                                        err->matrix_error.error,
-                                                        static_cast(err->status_code),
-                                                        static_cast(err->error_code));
+            http::client()->upload_keys(
+              olm::client()->create_upload_keys_request(),
+              [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
+                  if (err) {
+                      nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
+                                            err->matrix_error.error,
+                                            static_cast(err->status_code),
+                                            static_cast(err->error_code));
 
-                                  if (err->status_code < 400 || err->status_code >= 500)
-                                          return;
-                          }
+                      if (err->status_code < 400 || err->status_code >= 500)
+                          return;
+                  }
 
-                          // mark as published anyway, otherwise we may end up in a loop.
-                          olm::mark_keys_as_published();
-                  });
+                  // mark as published anyway, otherwise we may end up in a loop.
+                  olm::mark_keys_as_published();
+              });
+        } else if (count->second > 2 * MAX_ONETIME_KEYS) {
+            nhlog::crypto()->warn("too many one-time keys, deleting 1");
+            mtx::requests::ClaimKeys req;
+            req.one_time_keys[http::client()->user_id().to_string()][http::client()->device_id()] =
+              std::string(mtx::crypto::SIGNED_CURVE25519);
+            http::client()->claim_keys(
+              req, [](const mtx::responses::ClaimKeys &, mtx::http::RequestErr err) {
+                  if (err)
+                      nhlog::crypto()->warn("failed to clear 1 one-time key: {} {} {}",
+                                            err->matrix_error.error,
+                                            static_cast(err->status_code),
+                                            static_cast(err->error_code));
+                  else
+                      nhlog::crypto()->info("cleared 1 one-time key");
+              });
         }
+    }
 }
 
 void
 ChatPage::getProfileInfo()
 {
-        const auto userid = utils::localUser().toStdString();
+    const auto userid = utils::localUser().toStdString();
 
-        http::client()->get_profile(
-          userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to retrieve own profile info");
-                          return;
+    http::client()->get_profile(
+      userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("failed to retrieve own profile info");
+              return;
+          }
+
+          emit setUserDisplayName(QString::fromStdString(res.display_name));
+
+          emit setUserAvatar(QString::fromStdString(res.avatar_url));
+      });
+}
+
+void
+ChatPage::getBackupVersion()
+{
+    if (!UserSettings::instance()->useOnlineKeyBackup()) {
+        nhlog::crypto()->info("Online key backup disabled.");
+        return;
+    }
+
+    http::client()->backup_version(
+      [this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) {
+          if (err) {
+              nhlog::net()->warn("Failed to retrieve backup version");
+              if (err->status_code == 404)
+                  cache::client()->deleteBackupVersion();
+              return;
+          }
+
+          // switch to UI thread for secrets stuff
+          QTimer::singleShot(0, this, [res] {
+              auto auth_data = nlohmann::json::parse(res.auth_data);
+
+              if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
+                  auto key = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1);
+                  if (!key) {
+                      nhlog::crypto()->info("No key for online key backup.");
+                      cache::client()->deleteBackupVersion();
+                      return;
                   }
 
-                  emit setUserDisplayName(QString::fromStdString(res.display_name));
+                  using namespace mtx::crypto;
+                  auto pubkey = CURVE25519_public_key_from_private(to_binary_buf(base642bin(*key)));
 
-                  emit setUserAvatar(QString::fromStdString(res.avatar_url));
+                  if (auth_data["public_key"].get() != pubkey) {
+                      nhlog::crypto()->info("Our backup key {} does not match the one "
+                                            "used in the online backup {}",
+                                            pubkey,
+                                            auth_data["public_key"]);
+                      cache::client()->deleteBackupVersion();
+                      return;
+                  }
+
+                  nhlog::crypto()->info("Using online key backup.");
+                  OnlineBackupVersion data{};
+                  data.algorithm = res.algorithm;
+                  data.version   = res.version;
+                  cache::client()->saveBackupVersion(data);
+              } else {
+                  nhlog::crypto()->info("Unsupported key backup algorithm: {}", res.algorithm);
+                  cache::client()->deleteBackupVersion();
+              }
           });
+      });
 }
 
 void
 ChatPage::initiateLogout()
 {
-        http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) {
-                if (err) {
-                        // TODO: handle special errors
-                        emit contentLoaded();
-                        nhlog::net()->warn("failed to logout: {} - {}",
-                                           mtx::errors::to_string(err->matrix_error.errcode),
-                                           err->matrix_error.error);
-                        return;
-                }
+    http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) {
+        if (err) {
+            // TODO: handle special errors
+            emit contentLoaded();
+            nhlog::net()->warn("failed to logout: {} - {}",
+                               mtx::errors::to_string(err->matrix_error.errcode),
+                               err->matrix_error.error);
+            return;
+        }
 
-                emit loggedOut();
-        });
+        emit loggedOut();
+    });
 
-        emit showOverlayProgressBar();
+    emit showOverlayProgressBar();
 }
 
 template
 void
 ChatPage::connectCallMessage()
 {
-        connect(callManager_,
-                qOverload(&CallManager::newMessage),
-                view_manager_,
-                qOverload(&TimelineViewManager::queueCallMessage));
+    connect(callManager_,
+            qOverload(&CallManager::newMessage),
+            view_manager_,
+            qOverload(&TimelineViewManager::queueCallMessage));
 }
 
 void
 ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
                                    const SecretsToDecrypt &secrets)
 {
-        QString text = QInputDialog::getText(
+    QString text = QInputDialog::getText(
+      ChatPage::instance(),
+      QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
+      keyDesc.name.empty()
+        ? QCoreApplication::translate(
+            "CrossSigningSecrets", "Enter your recovery key or passphrase to decrypt your secrets:")
+        : QCoreApplication::translate(
+            "CrossSigningSecrets",
+            "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
+            .arg(QString::fromStdString(keyDesc.name)),
+      QLineEdit::Password);
+
+    if (text.isEmpty())
+        return;
+
+    auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
+
+    if (!decryptionKey && keyDesc.passphrase) {
+        try {
+            decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
+        } catch (std::exception &e) {
+            nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", e.what());
+        }
+    }
+
+    if (!decryptionKey) {
+        QMessageBox::information(
           ChatPage::instance(),
-          QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
-          keyDesc.name.empty()
-            ? QCoreApplication::translate(
-                "CrossSigningSecrets",
-                "Enter your recovery key or passphrase to decrypt your secrets:")
-            : QCoreApplication::translate(
-                "CrossSigningSecrets",
-                "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
-                .arg(QString::fromStdString(keyDesc.name)),
-          QLineEdit::Password);
+          QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
+          QCoreApplication::translate("CrossSigningSecrets",
+                                      "Failed to decrypt secrets with the "
+                                      "provided recovery key or passphrase"));
+        return;
+    }
 
-        if (text.isEmpty())
-                return;
+    auto deviceKeys = cache::client()->userKeys(http::client()->user_id().to_string());
+    mtx::requests::KeySignaturesUpload req;
 
-        auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
+    for (const auto &[secretName, encryptedSecret] : secrets) {
+        auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
+        if (!decrypted.empty()) {
+            cache::storeSecret(secretName, decrypted);
 
-        if (!decryptionKey && keyDesc.passphrase) {
-                try {
-                        decryptionKey =
-                          mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
-                } catch (std::exception &e) {
-                        nhlog::crypto()->error("Failed to derive secret key from passphrase: {}",
-                                               e.what());
+            if (deviceKeys &&
+                secretName == mtx::secret_storage::secrets::cross_signing_self_signing) {
+                auto myKey = deviceKeys->device_keys.at(http::client()->device_id());
+                if (myKey.user_id == http::client()->user_id().to_string() &&
+                    myKey.device_id == http::client()->device_id() &&
+                    myKey.keys["ed25519:" + http::client()->device_id()] ==
+                      olm::client()->identity_keys().ed25519 &&
+                    myKey.keys["curve25519:" + http::client()->device_id()] ==
+                      olm::client()->identity_keys().curve25519) {
+                    json j = myKey;
+                    j.erase("signatures");
+                    j.erase("unsigned");
+
+                    auto ssk = mtx::crypto::PkSigning::from_seed(decrypted);
+                    myKey.signatures[http::client()->user_id().to_string()]
+                                    ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump());
+                    req.signatures[http::client()->user_id().to_string()]
+                                  [http::client()->device_id()] = myKey;
                 }
-        }
+            } else if (deviceKeys &&
+                       secretName == mtx::secret_storage::secrets::cross_signing_master) {
+                auto mk = mtx::crypto::PkSigning::from_seed(decrypted);
 
-        if (!decryptionKey) {
-                QMessageBox::information(
-                  ChatPage::instance(),
-                  QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
-                  QCoreApplication::translate("CrossSigningSecrets",
-                                              "Failed to decrypt secrets with the "
-                                              "provided recovery key or passphrase"));
-                return;
+                if (deviceKeys->master_keys.user_id == http::client()->user_id().to_string() &&
+                    deviceKeys->master_keys.keys["ed25519:" + mk.public_key()] == mk.public_key()) {
+                    json j = deviceKeys->master_keys;
+                    j.erase("signatures");
+                    j.erase("unsigned");
+                    mtx::crypto::CrossSigningKeys master_key = j;
+                    master_key.signatures[http::client()->user_id().to_string()]
+                                         ["ed25519:" + http::client()->device_id()] =
+                      olm::client()->sign_message(j.dump());
+                    req.signatures[http::client()->user_id().to_string()][mk.public_key()] =
+                      master_key;
+                }
+            }
         }
+    }
 
-        for (const auto &[secretName, encryptedSecret] : secrets) {
-                auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
-                if (!decrypted.empty())
-                        cache::storeSecret(secretName, decrypted);
-        }
+    if (!req.signatures.empty())
+        http::client()->keys_signatures_upload(
+          req, [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) {
+              if (err) {
+                  nhlog::net()->error("failed to upload signatures: {},{}",
+                                      mtx::errors::to_string(err->matrix_error.errcode),
+                                      static_cast(err->status_code));
+              }
+
+              for (const auto &[user_id, tmp] : res.errors)
+                  for (const auto &[key_id, e] : tmp)
+                      nhlog::net()->error("signature error for user '{}' and key "
+                                          "id {}: {}, {}",
+                                          user_id,
+                                          key_id,
+                                          mtx::errors::to_string(e.errcode),
+                                          e.error);
+          });
 }
 
 void
 ChatPage::startChat(QString userid)
 {
-        auto joined_rooms = cache::joinedRooms();
-        auto room_infos   = cache::getRoomInfo(joined_rooms);
+    auto joined_rooms = cache::joinedRooms();
+    auto room_infos   = cache::getRoomInfo(joined_rooms);
 
-        for (std::string room_id : joined_rooms) {
-                if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
-                        auto room_members = cache::roomMembers(room_id);
-                        if (std::find(room_members.begin(),
-                                      room_members.end(),
-                                      (userid).toStdString()) != room_members.end()) {
-                                view_manager_->rooms()->setCurrentRoom(
-                                  QString::fromStdString(room_id));
-                                return;
-                        }
-                }
-        }
-
-        if (QMessageBox::Yes !=
-            QMessageBox::question(
-              this,
-              tr("Confirm invite"),
-              tr("Do you really want to start a private chat with %1?").arg(userid)))
+    for (std::string room_id : joined_rooms) {
+        if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
+            auto room_members = cache::roomMembers(room_id);
+            if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) !=
+                room_members.end()) {
+                view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
                 return;
-
-        mtx::requests::CreateRoom req;
-        req.preset     = mtx::requests::Preset::PrivateChat;
-        req.visibility = mtx::common::RoomVisibility::Private;
-        if (utils::localUser() != userid) {
-                req.invite    = {userid.toStdString()};
-                req.is_direct = true;
+            }
         }
-        emit ChatPage::instance()->createRoom(req);
+    }
+
+    if (QMessageBox::Yes !=
+        QMessageBox::question(
+          this,
+          tr("Confirm invite"),
+          tr("Do you really want to start a private chat with %1?").arg(userid)))
+        return;
+
+    mtx::requests::CreateRoom req;
+    req.preset     = mtx::requests::Preset::PrivateChat;
+    req.visibility = mtx::common::RoomVisibility::Private;
+    if (utils::localUser() != userid) {
+        req.invite    = {userid.toStdString()};
+        req.is_direct = true;
+    }
+    emit ChatPage::instance()->createRoom(req);
 }
 
 static QString
 mxidFromSegments(QStringRef sigil, QStringRef mxid)
 {
-        if (mxid.isEmpty())
-                return "";
+    if (mxid.isEmpty())
+        return "";
 
-        auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
+    auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
 
-        if (sigil == "u") {
-                return "@" + mxid_;
-        } else if (sigil == "roomid") {
-                return "!" + mxid_;
-        } else if (sigil == "r") {
-                return "#" + mxid_;
-                //} else if (sigil == "group") {
-                //        return "+" + mxid_;
-        } else {
-                return "";
-        }
+    if (sigil == "u") {
+        return "@" + mxid_;
+    } else if (sigil == "roomid") {
+        return "!" + mxid_;
+    } else if (sigil == "r") {
+        return "#" + mxid_;
+        //} else if (sigil == "group") {
+        //        return "+" + mxid_;
+    } else {
+        return "";
+    }
 }
 
-void
+bool
 ChatPage::handleMatrixUri(const QByteArray &uri)
 {
-        nhlog::ui()->info("Received uri! {}", uri.toStdString());
-        QUrl uri_{QString::fromUtf8(uri)};
+    nhlog::ui()->info("Received uri! {}", uri.toStdString());
+    QUrl uri_{QString::fromUtf8(uri)};
 
-        if (uri_.scheme() != "matrix")
-                return;
+    // Convert matrix.to URIs to proper format
+    if (uri_.scheme() == "https" && uri_.host() == "matrix.to") {
+        QString p = uri_.fragment(QUrl::FullyEncoded);
+        if (p.startsWith("/"))
+            p.remove(0, 1);
 
-        auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
-        if (tempPath.startsWith('/'))
-                tempPath.remove(0, 1);
-        auto segments = tempPath.splitRef('/');
+        auto temp = p.split("?");
+        QString query;
+        if (temp.size() >= 2)
+            query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
 
-        if (segments.size() != 2 && segments.size() != 4)
-                return;
+        temp            = temp.first().split("/");
+        auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
+        QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
+        if (!identifier.isEmpty()) {
+            if (identifier.startsWith("@")) {
+                QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                if (!query.isEmpty())
+                    newUri.append("?" + query.toUtf8());
+                return handleMatrixUri(QUrl::fromEncoded(newUri));
+            } else if (identifier.startsWith("#")) {
+                QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                if (!eventId.isEmpty())
+                    newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
+                if (!query.isEmpty())
+                    newUri.append("?" + query.toUtf8());
+                return handleMatrixUri(QUrl::fromEncoded(newUri));
+            } else if (identifier.startsWith("!")) {
+                QByteArray newUri =
+                  "matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
+                if (!eventId.isEmpty())
+                    newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
+                if (!query.isEmpty())
+                    newUri.append("?" + query.toUtf8());
+                return handleMatrixUri(QUrl::fromEncoded(newUri));
+            }
+        }
+    }
 
-        auto sigil1 = segments[0];
-        auto mxid1  = mxidFromSegments(sigil1, segments[1]);
-        if (mxid1.isEmpty())
-                return;
+    // non-matrix URIs are not handled by us, return false
+    if (uri_.scheme() != "matrix")
+        return false;
 
-        QString mxid2;
-        if (segments.size() == 4 && segments[2] == "e") {
-                if (segments[3].isEmpty())
-                        return;
-                else
-                        mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
+    auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
+    if (tempPath.startsWith('/'))
+        tempPath.remove(0, 1);
+    auto segments = tempPath.splitRef('/');
+
+    if (segments.size() != 2 && segments.size() != 4)
+        return false;
+
+    auto sigil1 = segments[0];
+    auto mxid1  = mxidFromSegments(sigil1, segments[1]);
+    if (mxid1.isEmpty())
+        return false;
+
+    QString mxid2;
+    if (segments.size() == 4 && segments[2] == "e") {
+        if (segments[3].isEmpty())
+            return false;
+        else
+            mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
+    }
+
+    std::vector vias;
+    QString action;
+
+    for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) {
+        nhlog::ui()->info("item: {}", item.toStdString());
+
+        if (item.startsWith("action=")) {
+            action = item.remove("action=");
+        } else if (item.startsWith("via=")) {
+            vias.push_back(QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
+        }
+    }
+
+    if (sigil1 == "u") {
+        if (action.isEmpty()) {
+            auto t = view_manager_->rooms()->currentRoom();
+            if (t && cache::isRoomMember(mxid1.toStdString(), t->roomId().toStdString())) {
+                t->openUserProfile(mxid1);
+                return true;
+            }
+            emit view_manager_->openGlobalUserProfile(mxid1);
+        } else if (action == "chat") {
+            this->startChat(mxid1);
+        }
+        return true;
+    } else if (sigil1 == "roomid") {
+        auto joined_rooms = cache::joinedRooms();
+        auto targetRoomId = mxid1.toStdString();
+
+        for (auto roomid : joined_rooms) {
+            if (roomid == targetRoomId) {
+                view_manager_->rooms()->setCurrentRoom(mxid1);
+                if (!mxid2.isEmpty())
+                    view_manager_->showEvent(mxid1, mxid2);
+                return true;
+            }
         }
 
-        std::vector vias;
-        QString action;
+        if (action == "join" || action.isEmpty()) {
+            joinRoomVia(targetRoomId, vias);
+            return true;
+        }
+        return false;
+    } else if (sigil1 == "r") {
+        auto joined_rooms    = cache::joinedRooms();
+        auto targetRoomAlias = mxid1.toStdString();
 
-        for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) {
-                nhlog::ui()->info("item: {}", item.toStdString());
-
-                if (item.startsWith("action=")) {
-                        action = item.remove("action=");
-                } else if (item.startsWith("via=")) {
-                        vias.push_back(
-                          QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
+        for (auto roomid : joined_rooms) {
+            auto aliases = cache::client()->getRoomAliases(roomid);
+            if (aliases) {
+                if (aliases->alias == targetRoomAlias) {
+                    view_manager_->rooms()->setCurrentRoom(QString::fromStdString(roomid));
+                    if (!mxid2.isEmpty())
+                        view_manager_->showEvent(QString::fromStdString(roomid), mxid2);
+                    return true;
                 }
+            }
         }
 
-        if (sigil1 == "u") {
-                if (action.isEmpty()) {
-                        if (auto t = view_manager_->rooms()->currentRoom())
-                                t->openUserProfile(mxid1);
-                } else if (action == "chat") {
-                        this->startChat(mxid1);
-                }
-        } else if (sigil1 == "roomid") {
-                auto joined_rooms = cache::joinedRooms();
-                auto targetRoomId = mxid1.toStdString();
-
-                for (auto roomid : joined_rooms) {
-                        if (roomid == targetRoomId) {
-                                view_manager_->rooms()->setCurrentRoom(mxid1);
-                                if (!mxid2.isEmpty())
-                                        view_manager_->showEvent(mxid1, mxid2);
-                                return;
-                        }
-                }
-
-                if (action == "join" || action.isEmpty()) {
-                        joinRoomVia(targetRoomId, vias);
-                }
-        } else if (sigil1 == "r") {
-                auto joined_rooms    = cache::joinedRooms();
-                auto targetRoomAlias = mxid1.toStdString();
-
-                for (auto roomid : joined_rooms) {
-                        auto aliases = cache::client()->getRoomAliases(roomid);
-                        if (aliases) {
-                                if (aliases->alias == targetRoomAlias) {
-                                        view_manager_->rooms()->setCurrentRoom(
-                                          QString::fromStdString(roomid));
-                                        if (!mxid2.isEmpty())
-                                                view_manager_->showEvent(
-                                                  QString::fromStdString(roomid), mxid2);
-                                        return;
-                                }
-                        }
-                }
-
-                if (action == "join" || action.isEmpty()) {
-                        joinRoomVia(mxid1.toStdString(), vias);
-                }
+        if (action == "join" || action.isEmpty()) {
+            joinRoomVia(mxid1.toStdString(), vias);
+            return true;
         }
+        return false;
+    }
+    return false;
 }
 
-void
+bool
 ChatPage::handleMatrixUri(const QUrl &uri)
 {
-        handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
+    return handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
 }
 
 bool
 ChatPage::isRoomActive(const QString &room_id)
 {
-        return isActiveWindow() && currentRoom() == room_id;
+    return isActiveWindow() && currentRoom() == room_id;
 }
 
 QString
 ChatPage::currentRoom() const
 {
-        if (view_manager_->rooms()->currentRoom())
-                return view_manager_->rooms()->currentRoom()->roomId();
-        else
-                return "";
+    if (view_manager_->rooms()->currentRoom())
+        return view_manager_->rooms()->currentRoom()->roomId();
+    else
+        return "";
 }
diff --git a/src/ChatPage.h b/src/ChatPage.h
index c90b87f5..8f3dc53e 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -52,183 +52,181 @@ using SecretsToDecrypt = std::map userSettings, QWidget *parent = nullptr);
+    ChatPage(QSharedPointer 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() { return userSettings_; }
-        CallManager *callManager() { return callManager_; }
-        TimelineViewManager *timelineManager() { return view_manager_; }
-        void deleteConfigs();
+    QSharedPointer 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 &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 &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 ¬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 ¬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 &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 &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 &counts);
-        void getProfileInfo();
+    void startInitialSync();
+    void tryInitialSync();
+    void trySync();
+    void verifyOneTimeKeyCountAfterStartup();
+    void ensureOneTimeKeyCount(const std::map &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;
-        using Memberships = std::map;
+    using UserID      = QString;
+    using Membership  = mtx::events::StateEvent;
+    using Memberships = std::map;
 
-        void loadStateFromCache();
-        void resetUI();
+    void loadStateFromCache();
+    void resetUI();
 
-        template
-        Memberships getMemberships(const std::vector &events) const;
+    template
+    Memberships getMemberships(const std::vector &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
-        void connectCallMessage();
+    template
+    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_;
+    // Global user settings.
+    QSharedPointer userSettings_;
 
-        NotificationsManager notificationsManager;
-        CallManager *callManager_;
+    NotificationsManager notificationsManager;
+    CallManager *callManager_;
 };
 
 template
 std::map>
 ChatPage::getMemberships(const std::vector &collection) const
 {
-        std::map> memberships;
+    std::map> memberships;
 
-        using Member = mtx::events::StateEvent;
+    using Member = mtx::events::StateEvent;
 
-        for (const auto &event : collection) {
-                if (auto member = std::get_if(event)) {
-                        memberships.emplace(member->state_key, *member);
-                }
+    for (const auto &event : collection) {
+        if (auto member = std::get_if(event)) {
+            memberships.emplace(member->state_key, *member);
         }
+    }
 
-        return memberships;
+    return memberships;
 }
diff --git a/src/Clipboard.cpp b/src/Clipboard.cpp
index d4d5bab7..93d913be 100644
--- a/src/Clipboard.cpp
+++ b/src/Clipboard.cpp
@@ -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();
 }
diff --git a/src/Clipboard.h b/src/Clipboard.h
index fa74da22..1a6584ca 100644
--- a/src/Clipboard.h
+++ b/src/Clipboard.h
@@ -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();
 };
diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp
index 41fd5d8f..9c371c8c 100644
--- a/src/ColorImageProvider.cpp
+++ b/src/ColorImageProvider.cpp
@@ -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;
 }
diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h
index 9ae8c85e..f2997e0a 100644
--- a/src/ColorImageProvider.h
+++ b/src/ColorImageProvider.h
@@ -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;
 };
diff --git a/src/CombinedImagePackModel.cpp b/src/CombinedImagePackModel.cpp
index 341a34ec..9a52f810 100644
--- a/src/CombinedImagePackModel.cpp
+++ b/src/CombinedImagePackModel.cpp
@@ -13,65 +13,65 @@ CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId,
   : QAbstractListModel(parent)
   , room_id(roomId)
 {
-        auto packs = cache::client()->getImagePacks(room_id, stickers);
+    auto packs = cache::client()->getImagePacks(room_id, stickers);
 
-        for (const auto &pack : packs) {
-                QString packname =
-                  pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : "";
+    for (const auto &pack : packs) {
+        QString packname =
+          pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : "";
 
-                for (const auto &img : pack.pack.images) {
-                        ImageDesc i{};
-                        i.shortcode = QString::fromStdString(img.first);
-                        i.packname  = packname;
-                        i.image     = img.second;
-                        images.push_back(std::move(i));
-                }
+        for (const auto &img : pack.pack.images) {
+            ImageDesc i{};
+            i.shortcode = QString::fromStdString(img.first);
+            i.packname  = packname;
+            i.image     = img.second;
+            images.push_back(std::move(i));
         }
+    }
 }
 
 int
 CombinedImagePackModel::rowCount(const QModelIndex &) const
 {
-        return (int)images.size();
+    return (int)images.size();
 }
 
 QHash
 CombinedImagePackModel::roleNames() const
 {
-        return {
-          {CompletionModel::CompletionRole, "completionRole"},
-          {CompletionModel::SearchRole, "searchRole"},
-          {CompletionModel::SearchRole2, "searchRole2"},
-          {Roles::Url, "url"},
-          {Roles::ShortCode, "shortcode"},
-          {Roles::Body, "body"},
-          {Roles::PackName, "packname"},
-          {Roles::OriginalRow, "originalRow"},
-        };
+    return {
+      {CompletionModel::CompletionRole, "completionRole"},
+      {CompletionModel::SearchRole, "searchRole"},
+      {CompletionModel::SearchRole2, "searchRole2"},
+      {Roles::Url, "url"},
+      {Roles::ShortCode, "shortcode"},
+      {Roles::Body, "body"},
+      {Roles::PackName, "packname"},
+      {Roles::OriginalRow, "originalRow"},
+    };
 }
 
 QVariant
 CombinedImagePackModel::data(const QModelIndex &index, int role) const
 {
-        if (hasIndex(index.row(), index.column(), index.parent())) {
-                switch (role) {
-                case CompletionModel::CompletionRole:
-                        return QString::fromStdString(images[index.row()].image.url);
-                case Roles::Url:
-                        return QString::fromStdString(images[index.row()].image.url);
-                case CompletionModel::SearchRole:
-                case Roles::ShortCode:
-                        return images[index.row()].shortcode;
-                case CompletionModel::SearchRole2:
-                case Roles::Body:
-                        return QString::fromStdString(images[index.row()].image.body);
-                case Roles::PackName:
-                        return images[index.row()].packname;
-                case Roles::OriginalRow:
-                        return index.row();
-                default:
-                        return {};
-                }
+    if (hasIndex(index.row(), index.column(), index.parent())) {
+        switch (role) {
+        case CompletionModel::CompletionRole:
+            return QString::fromStdString(images[index.row()].image.url);
+        case Roles::Url:
+            return QString::fromStdString(images[index.row()].image.url);
+        case CompletionModel::SearchRole:
+        case Roles::ShortCode:
+            return images[index.row()].shortcode;
+        case CompletionModel::SearchRole2:
+        case Roles::Body:
+            return QString::fromStdString(images[index.row()].image.body);
+        case Roles::PackName:
+            return images[index.row()].packname;
+        case Roles::OriginalRow:
+            return index.row();
+        default:
+            return {};
         }
-        return {};
+    }
+    return {};
 }
diff --git a/src/CombinedImagePackModel.h b/src/CombinedImagePackModel.h
index f0f69799..ec49b325 100644
--- a/src/CombinedImagePackModel.h
+++ b/src/CombinedImagePackModel.h
@@ -10,39 +10,39 @@
 
 class CombinedImagePackModel : public QAbstractListModel
 {
-        Q_OBJECT
+    Q_OBJECT
 public:
-        enum Roles
-        {
-                Url = Qt::UserRole,
-                ShortCode,
-                Body,
-                PackName,
-                OriginalRow,
-        };
+    enum Roles
+    {
+        Url = Qt::UserRole,
+        ShortCode,
+        Body,
+        PackName,
+        OriginalRow,
+    };
 
-        CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
-        QHash roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-        QVariant data(const QModelIndex &index, int role) const override;
+    CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
+    QHash roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+    QVariant data(const QModelIndex &index, int role) const override;
 
-        mtx::events::msc2545::PackImage imageAt(int row)
-        {
-                if (row < 0 || static_cast(row) >= images.size())
-                        return {};
-                return images.at(static_cast(row)).image;
-        }
+    mtx::events::msc2545::PackImage imageAt(int row)
+    {
+        if (row < 0 || static_cast(row) >= images.size())
+            return {};
+        return images.at(static_cast(row)).image;
+    }
 
 private:
-        std::string room_id;
+    std::string room_id;
 
-        struct ImageDesc
-        {
-                QString shortcode;
-                QString packname;
+    struct ImageDesc
+    {
+        QString shortcode;
+        QString packname;
 
-                mtx::events::msc2545::PackImage image;
-        };
+        mtx::events::msc2545::PackImage image;
+    };
 
-        std::vector images;
+    std::vector images;
 };
diff --git a/src/CompletionModelRoles.h b/src/CompletionModelRoles.h
index 8505e761..9a735d60 100644
--- a/src/CompletionModelRoles.h
+++ b/src/CompletionModelRoles.h
@@ -12,8 +12,8 @@ namespace CompletionModel {
 // Start at Qt::UserRole * 2 to prevent clashes
 enum Roles
 {
-        CompletionRole = Qt::UserRole * 2, // The string to replace the active completion
-        SearchRole,                        // String completer uses for search
-        SearchRole2,                       // Secondary string completer uses for search
+    CompletionRole = Qt::UserRole * 2, // The string to replace the active completion
+    SearchRole,                        // String completer uses for search
+    SearchRole2,                       // Secondary string completer uses for search
 };
 }
diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp
index e68944c7..454f54b7 100644
--- a/src/CompletionProxyModel.cpp
+++ b/src/CompletionProxyModel.cpp
@@ -18,154 +18,154 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
   , maxMistakes_(max_mistakes)
   , max_completions_(max_completions)
 {
-        setSourceModel(model);
-        QChar splitPoints(' ');
+    setSourceModel(model);
+    QChar splitPoints(' ');
 
-        // insert all the full texts
-        for (int i = 0; i < sourceModel()->rowCount(); i++) {
-                if (static_cast(i) < max_completions_)
-                        mapping.push_back(i);
+    // insert all the full texts
+    for (int i = 0; i < sourceModel()->rowCount(); i++) {
+        if (static_cast(i) < max_completions_)
+            mapping.push_back(i);
 
-                auto string1 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
-                                 .toString()
-                                 .toLower();
-                if (!string1.isEmpty())
-                        trie_.insert(string1.toUcs4(), i);
+        auto string1 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
+                         .toString()
+                         .toLower();
+        if (!string1.isEmpty())
+            trie_.insert(string1.toUcs4(), i);
 
-                auto string2 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
-                                 .toString()
-                                 .toLower();
-                if (!string2.isEmpty())
-                        trie_.insert(string2.toUcs4(), i);
+        auto string2 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
+                         .toString()
+                         .toLower();
+        if (!string2.isEmpty())
+            trie_.insert(string2.toUcs4(), i);
+    }
+
+    // insert the partial matches
+    for (int i = 0; i < sourceModel()->rowCount(); i++) {
+        auto string1 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
+                         .toString()
+                         .toLower();
+
+        for (const auto &e : string1.splitRef(splitPoints)) {
+            if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
+                trie_.insert(e.toUcs4(), i);
         }
 
-        // insert the partial matches
-        for (int i = 0; i < sourceModel()->rowCount(); i++) {
-                auto string1 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole)
-                                 .toString()
-                                 .toLower();
+        auto string2 = sourceModel()
+                         ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
+                         .toString()
+                         .toLower();
 
-                for (const auto &e : string1.splitRef(splitPoints)) {
-                        if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
-                                trie_.insert(e.toUcs4(), i);
-                }
-
-                auto string2 = sourceModel()
-                                 ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2)
-                                 .toString()
-                                 .toLower();
-
-                if (!string2.isEmpty()) {
-                        for (const auto &e : string2.splitRef(splitPoints)) {
-                                if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
-                                        trie_.insert(e.toUcs4(), i);
-                        }
-                }
+        if (!string2.isEmpty()) {
+            for (const auto &e : string2.splitRef(splitPoints)) {
+                if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
+                    trie_.insert(e.toUcs4(), i);
+            }
         }
+    }
 
-        connect(
-          this,
-          &CompletionProxyModel::newSearchString,
-          this,
-          [this](QString s) {
-                  s.remove(":");
-                  s.remove("@");
-                  searchString_ = s.toLower();
-                  invalidate();
-          },
-          Qt::QueuedConnection);
+    connect(
+      this,
+      &CompletionProxyModel::newSearchString,
+      this,
+      [this](QString s) {
+          s.remove(":");
+          s.remove("@");
+          searchString_ = s.toLower();
+          invalidate();
+      },
+      Qt::QueuedConnection);
 }
 
 void
 CompletionProxyModel::invalidate()
 {
-        auto key = searchString_.toUcs4();
-        beginResetModel();
-        if (!key.empty()) // return default model data, if no search string
-                mapping = trie_.search(key, max_completions_, maxMistakes_);
-        endResetModel();
+    auto key = searchString_.toUcs4();
+    beginResetModel();
+    if (!key.empty()) // return default model data, if no search string
+        mapping = trie_.search(key, max_completions_, maxMistakes_);
+    endResetModel();
 }
 
 QHash
 CompletionProxyModel::roleNames() const
 {
-        return this->sourceModel()->roleNames();
+    return this->sourceModel()->roleNames();
 }
 
 int
 CompletionProxyModel::rowCount(const QModelIndex &) const
 {
-        if (searchString_.isEmpty())
-                return std::min(static_cast(std::min(max_completions_,
-                                                                  std::numeric_limits::max())),
-                                sourceModel()->rowCount());
-        else
-                return (int)mapping.size();
+    if (searchString_.isEmpty())
+        return std::min(
+          static_cast(std::min(max_completions_, std::numeric_limits::max())),
+          sourceModel()->rowCount());
+    else
+        return (int)mapping.size();
 }
 
 QModelIndex
 CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
 {
-        // return default model data, if no search string
-        if (searchString_.isEmpty()) {
-                return index(sourceIndex.row(), 0);
-        }
+    // return default model data, if no search string
+    if (searchString_.isEmpty()) {
+        return index(sourceIndex.row(), 0);
+    }
 
-        for (int i = 0; i < (int)mapping.size(); i++) {
-                if (mapping[i] == sourceIndex.row()) {
-                        return index(i, 0);
-                }
+    for (int i = 0; i < (int)mapping.size(); i++) {
+        if (mapping[i] == sourceIndex.row()) {
+            return index(i, 0);
         }
-        return QModelIndex();
+    }
+    return QModelIndex();
 }
 
 QModelIndex
 CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const
 {
-        auto row = proxyIndex.row();
+    auto row = proxyIndex.row();
 
-        // return default model data, if no search string
-        if (searchString_.isEmpty()) {
-                return index(row, 0);
-        }
+    // return default model data, if no search string
+    if (searchString_.isEmpty()) {
+        return index(row, 0);
+    }
 
-        if (row < 0 || row >= (int)mapping.size())
-                return QModelIndex();
+    if (row < 0 || row >= (int)mapping.size())
+        return QModelIndex();
 
-        return sourceModel()->index(mapping[row], 0);
+    return sourceModel()->index(mapping[row], 0);
 }
 
 QModelIndex
 CompletionProxyModel::index(int row, int column, const QModelIndex &) const
 {
-        return createIndex(row, column);
+    return createIndex(row, column);
 }
 
 QModelIndex
 CompletionProxyModel::parent(const QModelIndex &) const
 {
-        return QModelIndex{};
+    return QModelIndex{};
 }
 int
 CompletionProxyModel::columnCount(const QModelIndex &) const
 {
-        return sourceModel()->columnCount();
+    return sourceModel()->columnCount();
 }
 
 QVariant
 CompletionProxyModel::completionAt(int i) const
 {
-        if (i >= 0 && i < rowCount())
-                return data(index(i, 0), CompletionModel::CompletionRole);
-        else
-                return {};
+    if (i >= 0 && i < rowCount())
+        return data(index(i, 0), CompletionModel::CompletionRole);
+    else
+        return {};
 }
 
 void
 CompletionProxyModel::setSearchString(QString s)
 {
-        emit newSearchString(s);
+    emit newSearchString(s);
 }
diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h
index d85d9343..c6331a2d 100644
--- a/src/CompletionProxyModel.h
+++ b/src/CompletionProxyModel.h
@@ -11,179 +11,176 @@
 template
 struct trie
 {
-        std::vector values;
-        std::map next;
+    std::vector values;
+    std::map next;
 
-        void insert(const QVector &keys, const Value &v)
-        {
-                auto t = this;
-                for (const auto k : keys) {
-                        t = &t->next[k];
-                }
-
-                t->values.push_back(v);
+    void insert(const QVector &keys, const Value &v)
+    {
+        auto t = this;
+        for (const auto k : keys) {
+            t = &t->next[k];
         }
 
-        std::vector valuesAndSubvalues(size_t limit = -1) const
-        {
-                std::vector ret;
-                if (limit < 200)
-                        ret.reserve(limit);
+        t->values.push_back(v);
+    }
 
-                for (const auto &v : values) {
-                        if (ret.size() >= limit)
-                                return ret;
-                        else
-                                ret.push_back(v);
-                }
-
-                for (const auto &[k, t] : next) {
-                        (void)k;
-                        if (ret.size() >= limit)
-                                return ret;
-                        else {
-                                auto temp = t.valuesAndSubvalues(limit - ret.size());
-                                for (auto &&v : temp) {
-                                        if (ret.size() >= limit)
-                                                return ret;
-
-                                        if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
-                                                ret.push_back(std::move(v));
-                                        }
-                                }
-                        }
-                }
+    std::vector valuesAndSubvalues(size_t limit = -1) const
+    {
+        std::vector ret;
+        if (limit < 200)
+            ret.reserve(limit);
 
+        for (const auto &v : values) {
+            if (ret.size() >= limit)
                 return ret;
+            else
+                ret.push_back(v);
         }
 
-        std::vector search(const QVector &keys, //< TODO(Nico): replace this with a span
-                                  size_t result_count_limit,
-                                  size_t max_edit_distance_ = 2) const
-        {
-                std::vector ret;
-                if (!result_count_limit)
+        for (const auto &[k, t] : next) {
+            (void)k;
+            if (ret.size() >= limit)
+                return ret;
+            else {
+                auto temp = t.valuesAndSubvalues(limit - ret.size());
+                for (auto &&v : temp) {
+                    if (ret.size() >= limit)
                         return ret;
 
-                if (keys.isEmpty())
-                        return valuesAndSubvalues(result_count_limit);
+                    if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
+                        ret.push_back(std::move(v));
+                    }
+                }
+            }
+        }
 
-                auto append = [&ret, result_count_limit](std::vector &&in) {
-                        for (auto &&v : in) {
-                                if (ret.size() >= result_count_limit)
-                                        return;
+        return ret;
+    }
 
-                                if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
-                                        ret.push_back(std::move(v));
-                                }
+    std::vector search(const QVector &keys, //< TODO(Nico): replace this with a span
+                              size_t result_count_limit,
+                              size_t max_edit_distance_ = 2) const
+    {
+        std::vector ret;
+        if (!result_count_limit)
+            return ret;
+
+        if (keys.isEmpty())
+            return valuesAndSubvalues(result_count_limit);
+
+        auto append = [&ret, result_count_limit](std::vector &&in) {
+            for (auto &&v : in) {
+                if (ret.size() >= result_count_limit)
+                    return;
+
+                if (std::find(ret.begin(), ret.end(), v) == ret.end()) {
+                    ret.push_back(std::move(v));
+                }
+            }
+        };
+
+        auto limit = [&ret, result_count_limit] {
+            return std::min(result_count_limit, (result_count_limit - ret.size()) * 2);
+        };
+
+        // Try first exact matches, then with maximum errors
+        for (size_t max_edit_distance = 0;
+             max_edit_distance <= max_edit_distance_ && ret.size() < result_count_limit;
+             max_edit_distance += 1) {
+            if (max_edit_distance && ret.size() < result_count_limit) {
+                max_edit_distance -= 1;
+
+                // swap chars case
+                if (keys.size() >= 2) {
+                    auto t = this;
+                    for (int i = 1; i >= 0; i--) {
+                        if (auto e = t->next.find(keys[i]); e != t->next.end()) {
+                            t = &e->second;
+                        } else {
+                            t = nullptr;
+                            break;
                         }
-                };
+                    }
 
-                auto limit = [&ret, result_count_limit] {
-                        return std::min(result_count_limit, (result_count_limit - ret.size()) * 2);
-                };
-
-                // Try first exact matches, then with maximum errors
-                for (size_t max_edit_distance = 0;
-                     max_edit_distance <= max_edit_distance_ && ret.size() < result_count_limit;
-                     max_edit_distance += 1) {
-                        if (max_edit_distance && ret.size() < result_count_limit) {
-                                max_edit_distance -= 1;
-
-                                // swap chars case
-                                if (keys.size() >= 2) {
-                                        auto t = this;
-                                        for (int i = 1; i >= 0; i--) {
-                                                if (auto e = t->next.find(keys[i]);
-                                                    e != t->next.end()) {
-                                                        t = &e->second;
-                                                } else {
-                                                        t = nullptr;
-                                                        break;
-                                                }
-                                        }
-
-                                        if (t) {
-                                                append(t->search(
-                                                  keys.mid(2), limit(), max_edit_distance));
-                                        }
-                                }
-
-                                // insert case
-                                for (const auto &[k, t] : this->next) {
-                                        if (k == keys[0])
-                                                continue;
-                                        if (ret.size() >= limit())
-                                                break;
-
-                                        // insert
-                                        append(t.search(keys, limit(), max_edit_distance));
-                                }
-
-                                // delete character case
-                                append(this->search(keys.mid(1), limit(), max_edit_distance));
-
-                                // substitute case
-                                for (const auto &[k, t] : this->next) {
-                                        if (k == keys[0])
-                                                continue;
-                                        if (ret.size() >= limit())
-                                                break;
-
-                                        // substitute
-                                        append(t.search(keys.mid(1), limit(), max_edit_distance));
-                                }
-
-                                max_edit_distance += 1;
-                        }
-
-                        if (auto e = this->next.find(keys[0]); e != this->next.end()) {
-                                append(e->second.search(keys.mid(1), limit(), max_edit_distance));
-                        }
+                    if (t) {
+                        append(t->search(keys.mid(2), limit(), max_edit_distance));
+                    }
                 }
 
-                return ret;
+                // insert case
+                for (const auto &[k, t] : this->next) {
+                    if (k == keys[0])
+                        continue;
+                    if (ret.size() >= limit())
+                        break;
+
+                    // insert
+                    append(t.search(keys, limit(), max_edit_distance));
+                }
+
+                // delete character case
+                append(this->search(keys.mid(1), limit(), max_edit_distance));
+
+                // substitute case
+                for (const auto &[k, t] : this->next) {
+                    if (k == keys[0])
+                        continue;
+                    if (ret.size() >= limit())
+                        break;
+
+                    // substitute
+                    append(t.search(keys.mid(1), limit(), max_edit_distance));
+                }
+
+                max_edit_distance += 1;
+            }
+
+            if (auto e = this->next.find(keys[0]); e != this->next.end()) {
+                append(e->second.search(keys.mid(1), limit(), max_edit_distance));
+            }
         }
+
+        return ret;
+    }
 };
 
 class CompletionProxyModel : public QAbstractProxyModel
 {
-        Q_OBJECT
-        Q_PROPERTY(
-          QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
+    Q_OBJECT
+    Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString)
 public:
-        CompletionProxyModel(QAbstractItemModel *model,
-                             int max_mistakes       = 2,
-                             size_t max_completions = 7,
-                             QObject *parent        = nullptr);
+    CompletionProxyModel(QAbstractItemModel *model,
+                         int max_mistakes       = 2,
+                         size_t max_completions = 7,
+                         QObject *parent        = nullptr);
 
-        void invalidate();
+    void invalidate();
 
-        QHash roleNames() const override;
-        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-        int columnCount(const QModelIndex &) const override;
+    QHash roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+    int columnCount(const QModelIndex &) const override;
 
-        QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
-        QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
+    QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
+    QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
 
-        QModelIndex index(int row,
-                          int column,
-                          const QModelIndex &parent = QModelIndex()) const override;
-        QModelIndex parent(const QModelIndex &) const override;
+    QModelIndex index(int row,
+                      int column,
+                      const QModelIndex &parent = QModelIndex()) const override;
+    QModelIndex parent(const QModelIndex &) const override;
 
 public slots:
-        QVariant completionAt(int i) const;
+    QVariant completionAt(int i) const;
 
-        void setSearchString(QString s);
-        QString searchString() const { return searchString_; }
+    void setSearchString(QString s);
+    QString searchString() const { return searchString_; }
 
 signals:
-        void newSearchString(QString);
+    void newSearchString(QString);
 
 private:
-        QString searchString_;
-        trie trie_;
-        std::vector mapping;
-        int maxMistakes_;
-        size_t max_completions_;
+    QString searchString_;
+    trie trie_;
+    std::vector mapping;
+    int maxMistakes_;
+    size_t max_completions_;
 };
diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp
deleted file mode 100644
index 1760ea9a..00000000
--- a/src/DeviceVerificationFlow.cpp
+++ /dev/null
@@ -1,882 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include "DeviceVerificationFlow.h"
-
-#include "Cache.h"
-#include "Cache_p.h"
-#include "ChatPage.h"
-#include "Logging.h"
-#include "Utils.h"
-#include "timeline/TimelineModel.h"
-
-#include 
-#include 
-#include 
-
-static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
-
-namespace msgs = mtx::events::msg;
-
-static mtx::events::msg::KeyVerificationMac
-key_verification_mac(mtx::crypto::SAS *sas,
-                     mtx::identifiers::User sender,
-                     const std::string &senderDevice,
-                     mtx::identifiers::User receiver,
-                     const std::string &receiverDevice,
-                     const std::string &transactionId,
-                     std::map keys);
-
-DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
-                                               DeviceVerificationFlow::Type flow_type,
-                                               TimelineModel *model,
-                                               QString userID,
-                                               QString deviceId_)
-  : sender(false)
-  , type(flow_type)
-  , deviceId(deviceId_)
-  , model_(model)
-{
-        timeout = new QTimer(this);
-        timeout->setSingleShot(true);
-        this->sas           = olm::client()->sas_init();
-        this->isMacVerified = false;
-
-        auto user_id   = userID.toStdString();
-        this->toClient = mtx::identifiers::parse(user_id);
-        cache::client()->query_keys(
-          user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to query device keys: {},{}",
-                                             mtx::errors::to_string(err->matrix_error.errcode),
-                                             static_cast(err->status_code));
-                          return;
-                  }
-
-                  if (!this->deviceId.isEmpty() &&
-                      (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
-                          nhlog::net()->warn("no devices retrieved {}", user_id);
-                          return;
-                  }
-
-                  this->their_keys = res;
-          });
-
-        cache::client()->query_keys(
-          http::client()->user_id().to_string(),
-          [this](const UserKeyCache &res, mtx::http::RequestErr err) {
-                  if (err) {
-                          nhlog::net()->warn("failed to query device keys: {},{}",
-                                             mtx::errors::to_string(err->matrix_error.errcode),
-                                             static_cast(err->status_code));
-                          return;
-                  }
-
-                  if (res.master_keys.keys.empty())
-                          return;
-
-                  if (auto status =
-                        cache::verificationStatus(http::client()->user_id().to_string());
-                      status && status->user_verified == crypto::Trust::Verified)
-                          this->our_trusted_master_key = res.master_keys.keys.begin()->second;
-          });
-
-        if (model) {
-                connect(this->model_,
-                        &TimelineModel::updateFlowEventId,
-                        this,
-                        [this](std::string event_id_) {
-                                this->relation.rel_type = mtx::common::RelationType::Reference;
-                                this->relation.event_id = event_id_;
-                                this->transaction_id    = event_id_;
-                        });
-        }
-
-        connect(timeout, &QTimer::timeout, this, [this]() {
-                nhlog::crypto()->info("verification: timeout");
-                if (state_ != Success && state_ != Failed)
-                        this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
-        });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationStart,
-                this,
-                &DeviceVerificationFlow::handleStartMessage);
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationAccept,
-                this,
-                [this](const mtx::events::msg::KeyVerificationAccept &msg) {
-                        nhlog::crypto()->info("verification: received accept");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-                        if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
-                            (msg.hash == "sha256") &&
-                            (msg.message_authentication_code == "hkdf-hmac-sha256")) {
-                                this->commitment = msg.commitment;
-                                if (std::find(msg.short_authentication_string.begin(),
-                                              msg.short_authentication_string.end(),
-                                              mtx::events::msg::SASMethods::Emoji) !=
-                                    msg.short_authentication_string.end()) {
-                                        this->method = mtx::events::msg::SASMethods::Emoji;
-                                } else {
-                                        this->method = mtx::events::msg::SASMethods::Decimal;
-                                }
-                                this->mac_method = msg.message_authentication_code;
-                                this->sendVerificationKey();
-                        } else {
-                                this->cancelVerification(
-                                  DeviceVerificationFlow::Error::UnknownMethod);
-                        }
-                });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationCancel,
-                this,
-                [this](const mtx::events::msg::KeyVerificationCancel &msg) {
-                        nhlog::crypto()->info("verification: received cancel");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-                        error_ = User;
-                        emit errorChanged();
-                        setState(Failed);
-                });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationKey,
-                this,
-                [this](const mtx::events::msg::KeyVerificationKey &msg) {
-                        nhlog::crypto()->info("verification: received key");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-
-                        if (sender) {
-                                if (state_ != WaitingForOtherToAccept) {
-                                        this->cancelVerification(OutOfOrder);
-                                        return;
-                                }
-                        } else {
-                                if (state_ != WaitingForKeys) {
-                                        this->cancelVerification(OutOfOrder);
-                                        return;
-                                }
-                        }
-
-                        this->sas->set_their_key(msg.key);
-                        std::string info;
-                        if (this->sender == true) {
-                                info = "MATRIX_KEY_VERIFICATION_SAS|" +
-                                       http::client()->user_id().to_string() + "|" +
-                                       http::client()->device_id() + "|" + this->sas->public_key() +
-                                       "|" + this->toClient.to_string() + "|" +
-                                       this->deviceId.toStdString() + "|" + msg.key + "|" +
-                                       this->transaction_id;
-                        } else {
-                                info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
-                                       "|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
-                                       http::client()->user_id().to_string() + "|" +
-                                       http::client()->device_id() + "|" + this->sas->public_key() +
-                                       "|" + this->transaction_id;
-                        }
-
-                        nhlog::ui()->info("Info is: '{}'", info);
-
-                        if (this->sender == false) {
-                                this->sendVerificationKey();
-                        } else {
-                                if (this->commitment !=
-                                    mtx::crypto::bin2base64_unpadded(
-                                      mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
-                                        this->cancelVerification(
-                                          DeviceVerificationFlow::Error::MismatchedCommitment);
-                                        return;
-                                }
-                        }
-
-                        if (this->method == mtx::events::msg::SASMethods::Emoji) {
-                                this->sasList = this->sas->generate_bytes_emoji(info);
-                                setState(CompareEmoji);
-                        } else if (this->method == mtx::events::msg::SASMethods::Decimal) {
-                                this->sasList = this->sas->generate_bytes_decimal(info);
-                                setState(CompareNumber);
-                        }
-                });
-
-        connect(
-          ChatPage::instance(),
-          &ChatPage::receivedDeviceVerificationMac,
-          this,
-          [this](const mtx::events::msg::KeyVerificationMac &msg) {
-                  nhlog::crypto()->info("verification: received mac");
-                  if (msg.transaction_id.has_value()) {
-                          if (msg.transaction_id.value() != this->transaction_id)
-                                  return;
-                  } else if (msg.relations.references()) {
-                          if (msg.relations.references() != this->relation.event_id)
-                                  return;
-                  }
-
-                  std::map key_list;
-                  std::string key_string;
-                  for (const auto &mac : msg.mac) {
-                          for (const auto &[deviceid, key] : their_keys.device_keys) {
-                                  (void)deviceid;
-                                  if (key.keys.count(mac.first))
-                                          key_list[mac.first] = key.keys.at(mac.first);
-                          }
-
-                          if (their_keys.master_keys.keys.count(mac.first))
-                                  key_list[mac.first] = their_keys.master_keys.keys[mac.first];
-                          if (their_keys.user_signing_keys.keys.count(mac.first))
-                                  key_list[mac.first] =
-                                    their_keys.user_signing_keys.keys[mac.first];
-                          if (their_keys.self_signing_keys.keys.count(mac.first))
-                                  key_list[mac.first] =
-                                    their_keys.self_signing_keys.keys[mac.first];
-                  }
-                  auto macs = key_verification_mac(sas.get(),
-                                                   toClient,
-                                                   this->deviceId.toStdString(),
-                                                   http::client()->user_id(),
-                                                   http::client()->device_id(),
-                                                   this->transaction_id,
-                                                   key_list);
-
-                  for (const auto &[key, mac] : macs.mac) {
-                          if (mac != msg.mac.at(key)) {
-                                  this->cancelVerification(
-                                    DeviceVerificationFlow::Error::KeyMismatch);
-                                  return;
-                          }
-                  }
-
-                  if (msg.keys == macs.keys) {
-                          mtx::requests::KeySignaturesUpload req;
-                          if (utils::localUser().toStdString() == this->toClient.to_string()) {
-                                  // self verification, sign master key with device key, if we
-                                  // verified it
-                                  for (const auto &mac : msg.mac) {
-                                          if (their_keys.master_keys.keys.count(mac.first)) {
-                                                  json j = their_keys.master_keys;
-                                                  j.erase("signatures");
-                                                  j.erase("unsigned");
-                                                  mtx::crypto::CrossSigningKeys master_key = j;
-                                                  master_key
-                                                    .signatures[utils::localUser().toStdString()]
-                                                               ["ed25519:" +
-                                                                http::client()->device_id()] =
-                                                    olm::client()->sign_message(j.dump());
-                                                  req.signatures[utils::localUser().toStdString()]
-                                                                [master_key.keys.at(mac.first)] =
-                                                    master_key;
-                                          } else if (mac.first ==
-                                                     "ed25519:" + this->deviceId.toStdString()) {
-                                                  // Sign their device key with self signing key
-
-                                                  auto device_id = this->deviceId.toStdString();
-
-                                                  if (their_keys.device_keys.count(device_id)) {
-                                                          json j =
-                                                            their_keys.device_keys.at(device_id);
-                                                          j.erase("signatures");
-                                                          j.erase("unsigned");
-
-                                                          auto secret = cache::secret(
-                                                            mtx::secret_storage::secrets::
-                                                              cross_signing_self_signing);
-                                                          if (!secret)
-                                                                  continue;
-                                                          auto ssk =
-                                                            mtx::crypto::PkSigning::from_seed(
-                                                              *secret);
-
-                                                          mtx::crypto::DeviceKeys dev = j;
-                                                          dev.signatures
-                                                            [utils::localUser().toStdString()]
-                                                            ["ed25519:" + ssk.public_key()] =
-                                                            ssk.sign(j.dump());
-
-                                                          req.signatures[utils::localUser()
-                                                                           .toStdString()]
-                                                                        [device_id] = dev;
-                                                  }
-                                          }
-                                  }
-                          } else {
-                                  // Sign their master key with user signing key
-                                  for (const auto &mac : msg.mac) {
-                                          if (their_keys.master_keys.keys.count(mac.first)) {
-                                                  json j = their_keys.master_keys;
-                                                  j.erase("signatures");
-                                                  j.erase("unsigned");
-
-                                                  auto secret =
-                                                    cache::secret(mtx::secret_storage::secrets::
-                                                                    cross_signing_user_signing);
-                                                  if (!secret)
-                                                          continue;
-                                                  auto usk =
-                                                    mtx::crypto::PkSigning::from_seed(*secret);
-
-                                                  mtx::crypto::CrossSigningKeys master_key = j;
-                                                  master_key
-                                                    .signatures[utils::localUser().toStdString()]
-                                                               ["ed25519:" + usk.public_key()] =
-                                                    usk.sign(j.dump());
-
-                                                  req.signatures[toClient.to_string()]
-                                                                [master_key.keys.at(mac.first)] =
-                                                    master_key;
-                                          }
-                                  }
-                          }
-
-                          if (!req.signatures.empty()) {
-                                  http::client()->keys_signatures_upload(
-                                    req,
-                                    [](const mtx::responses::KeySignaturesUpload &res,
-                                       mtx::http::RequestErr err) {
-                                            if (err) {
-                                                    nhlog::net()->error(
-                                                      "failed to upload signatures: {},{}",
-                                                      mtx::errors::to_string(
-                                                        err->matrix_error.errcode),
-                                                      static_cast(err->status_code));
-                                            }
-
-                                            for (const auto &[user_id, tmp] : res.errors)
-                                                    for (const auto &[key_id, e] : tmp)
-                                                            nhlog::net()->error(
-                                                              "signature error for user {} and key "
-                                                              "id {}: {}, {}",
-                                                              user_id,
-                                                              key_id,
-                                                              mtx::errors::to_string(e.errcode),
-                                                              e.error);
-                                    });
-                          }
-
-                          this->isMacVerified = true;
-                          this->acceptDevice();
-                  } else {
-                          this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
-                  }
-          });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationReady,
-                this,
-                [this](const mtx::events::msg::KeyVerificationReady &msg) {
-                        nhlog::crypto()->info("verification: received ready");
-                        if (!sender) {
-                                if (msg.from_device != http::client()->device_id()) {
-                                        error_ = User;
-                                        emit errorChanged();
-                                        setState(Failed);
-                                }
-
-                                return;
-                        }
-
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                                else {
-                                        this->deviceId = QString::fromStdString(msg.from_device);
-                                }
-                        }
-                        this->startVerificationRequest();
-                });
-
-        connect(ChatPage::instance(),
-                &ChatPage::receivedDeviceVerificationDone,
-                this,
-                [this](const mtx::events::msg::KeyVerificationDone &msg) {
-                        nhlog::crypto()->info("verification: receoved done");
-                        if (msg.transaction_id.has_value()) {
-                                if (msg.transaction_id.value() != this->transaction_id)
-                                        return;
-                        } else if (msg.relations.references()) {
-                                if (msg.relations.references() != this->relation.event_id)
-                                        return;
-                        }
-                        nhlog::ui()->info("Flow done on other side");
-                });
-
-        timeout->start(TIMEOUT);
-}
-
-QString
-DeviceVerificationFlow::state()
-{
-        switch (state_) {
-        case PromptStartVerification:
-                return "PromptStartVerification";
-        case CompareEmoji:
-                return "CompareEmoji";
-        case CompareNumber:
-                return "CompareNumber";
-        case WaitingForKeys:
-                return "WaitingForKeys";
-        case WaitingForOtherToAccept:
-                return "WaitingForOtherToAccept";
-        case WaitingForMac:
-                return "WaitingForMac";
-        case Success:
-                return "Success";
-        case Failed:
-                return "Failed";
-        default:
-                return "";
-        }
-}
-
-void
-DeviceVerificationFlow::next()
-{
-        if (sender) {
-                switch (state_) {
-                case PromptStartVerification:
-                        sendVerificationRequest();
-                        break;
-                case CompareEmoji:
-                case CompareNumber:
-                        sendVerificationMac();
-                        break;
-                case WaitingForKeys:
-                case WaitingForOtherToAccept:
-                case WaitingForMac:
-                case Success:
-                case Failed:
-                        nhlog::db()->error("verification: Invalid state transition!");
-                        break;
-                }
-        } else {
-                switch (state_) {
-                case PromptStartVerification:
-                        if (canonical_json.is_null())
-                                sendVerificationReady();
-                        else // legacy path without request and ready
-                                acceptVerificationRequest();
-                        break;
-                case CompareEmoji:
-                        [[fallthrough]];
-                case CompareNumber:
-                        sendVerificationMac();
-                        break;
-                case WaitingForKeys:
-                case WaitingForOtherToAccept:
-                case WaitingForMac:
-                case Success:
-                case Failed:
-                        nhlog::db()->error("verification: Invalid state transition!");
-                        break;
-                }
-        }
-}
-
-QString
-DeviceVerificationFlow::getUserId()
-{
-        return QString::fromStdString(this->toClient.to_string());
-}
-
-QString
-DeviceVerificationFlow::getDeviceId()
-{
-        return this->deviceId;
-}
-
-bool
-DeviceVerificationFlow::getSender()
-{
-        return this->sender;
-}
-
-std::vector
-DeviceVerificationFlow::getSasList()
-{
-        return this->sasList;
-}
-
-bool
-DeviceVerificationFlow::isSelfVerification() const
-{
-        return this->toClient.to_string() == http::client()->user_id().to_string();
-}
-
-void
-DeviceVerificationFlow::setEventId(std::string event_id_)
-{
-        this->relation.rel_type = mtx::common::RelationType::Reference;
-        this->relation.event_id = event_id_;
-        this->transaction_id    = event_id_;
-}
-
-void
-DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
-                                           std::string)
-{
-        if (msg.transaction_id.has_value()) {
-                if (msg.transaction_id.value() != this->transaction_id)
-                        return;
-        } else if (msg.relations.references()) {
-                if (msg.relations.references() != this->relation.event_id)
-                        return;
-        }
-        if ((std::find(msg.key_agreement_protocols.begin(),
-                       msg.key_agreement_protocols.end(),
-                       "curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
-            (std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
-            (std::find(msg.message_authentication_codes.begin(),
-                       msg.message_authentication_codes.end(),
-                       "hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
-                if (std::find(msg.short_authentication_string.begin(),
-                              msg.short_authentication_string.end(),
-                              mtx::events::msg::SASMethods::Emoji) !=
-                    msg.short_authentication_string.end()) {
-                        this->method = mtx::events::msg::SASMethods::Emoji;
-                } else if (std::find(msg.short_authentication_string.begin(),
-                                     msg.short_authentication_string.end(),
-                                     mtx::events::msg::SASMethods::Decimal) !=
-                           msg.short_authentication_string.end()) {
-                        this->method = mtx::events::msg::SASMethods::Decimal;
-                } else {
-                        this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
-                        return;
-                }
-                if (!sender)
-                        this->canonical_json = nlohmann::json(msg);
-                else {
-                        if (utils::localUser().toStdString() < this->toClient.to_string()) {
-                                this->canonical_json = nlohmann::json(msg);
-                        }
-                }
-
-                if (state_ != PromptStartVerification)
-                        this->acceptVerificationRequest();
-        } else {
-                this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
-        }
-}
-
-//! accepts a verification
-void
-DeviceVerificationFlow::acceptVerificationRequest()
-{
-        mtx::events::msg::KeyVerificationAccept req;
-
-        req.method                      = mtx::events::msg::VerificationMethods::SASv1;
-        req.key_agreement_protocol      = "curve25519-hkdf-sha256";
-        req.hash                        = "sha256";
-        req.message_authentication_code = "hkdf-hmac-sha256";
-        if (this->method == mtx::events::msg::SASMethods::Emoji)
-                req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
-        else if (this->method == mtx::events::msg::SASMethods::Decimal)
-                req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
-        req.commitment = mtx::crypto::bin2base64_unpadded(
-          mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
-
-        send(req);
-        setState(WaitingForKeys);
-}
-//! responds verification request
-void
-DeviceVerificationFlow::sendVerificationReady()
-{
-        mtx::events::msg::KeyVerificationReady req;
-
-        req.from_device = http::client()->device_id();
-        req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
-
-        send(req);
-        setState(WaitingForKeys);
-}
-//! accepts a verification
-void
-DeviceVerificationFlow::sendVerificationDone()
-{
-        mtx::events::msg::KeyVerificationDone req;
-
-        send(req);
-}
-//! starts the verification flow
-void
-DeviceVerificationFlow::startVerificationRequest()
-{
-        mtx::events::msg::KeyVerificationStart req;
-
-        req.from_device                  = http::client()->device_id();
-        req.method                       = mtx::events::msg::VerificationMethods::SASv1;
-        req.key_agreement_protocols      = {"curve25519-hkdf-sha256"};
-        req.hashes                       = {"sha256"};
-        req.message_authentication_codes = {"hkdf-hmac-sha256"};
-        req.short_authentication_string  = {mtx::events::msg::SASMethods::Decimal,
-                                           mtx::events::msg::SASMethods::Emoji};
-
-        if (this->type == DeviceVerificationFlow::Type::ToDevice) {
-                mtx::requests::ToDeviceMessages body;
-                req.transaction_id   = this->transaction_id;
-                this->canonical_json = nlohmann::json(req);
-        } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
-                req.relations.relations.push_back(this->relation);
-                // Set synthesized to surpress the nheko relation extensions
-                req.relations.synthesized = true;
-                this->canonical_json      = nlohmann::json(req);
-        }
-        send(req);
-        setState(WaitingForOtherToAccept);
-}
-//! sends a verification request
-void
-DeviceVerificationFlow::sendVerificationRequest()
-{
-        mtx::events::msg::KeyVerificationRequest req;
-
-        req.from_device = http::client()->device_id();
-        req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
-
-        if (this->type == DeviceVerificationFlow::Type::ToDevice) {
-                QDateTime currentTime = QDateTime::currentDateTimeUtc();
-
-                req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
-
-        } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
-                req.to      = this->toClient.to_string();
-                req.msgtype = "m.key.verification.request";
-                req.body = "User is requesting to verify keys with you. However, your client does "
-                           "not support this method, so you will need to use the legacy method of "
-                           "key verification.";
-        }
-
-        send(req);
-        setState(WaitingForOtherToAccept);
-}
-//! cancels a verification flow
-void
-DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
-{
-        if (state_ == State::Success || state_ == State::Failed)
-                return;
-
-        mtx::events::msg::KeyVerificationCancel req;
-
-        if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
-                req.code   = "m.unknown_method";
-                req.reason = "unknown method received";
-        } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
-                req.code   = "m.mismatched_commitment";
-                req.reason = "commitment didn't match";
-        } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
-                req.code   = "m.mismatched_sas";
-                req.reason = "sas didn't match";
-        } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
-                req.code   = "m.key_match";
-                req.reason = "keys did not match";
-        } else if (error_code == DeviceVerificationFlow::Error::Timeout) {
-                req.code   = "m.timeout";
-                req.reason = "timed out";
-        } else if (error_code == DeviceVerificationFlow::Error::User) {
-                req.code   = "m.user";
-                req.reason = "user cancelled the verification";
-        } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
-                req.code   = "m.unexpected_message";
-                req.reason = "received messages out of order";
-        }
-
-        this->error_ = error_code;
-        emit errorChanged();
-        this->setState(Failed);
-
-        send(req);
-}
-//! sends the verification key
-void
-DeviceVerificationFlow::sendVerificationKey()
-{
-        mtx::events::msg::KeyVerificationKey req;
-
-        req.key = this->sas->public_key();
-
-        send(req);
-}
-
-mtx::events::msg::KeyVerificationMac
-key_verification_mac(mtx::crypto::SAS *sas,
-                     mtx::identifiers::User sender,
-                     const std::string &senderDevice,
-                     mtx::identifiers::User receiver,
-                     const std::string &receiverDevice,
-                     const std::string &transactionId,
-                     std::map keys)
-{
-        mtx::events::msg::KeyVerificationMac req;
-
-        std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
-                           receiver.to_string() + receiverDevice + transactionId;
-
-        std::string key_list;
-        bool first = true;
-        for (const auto &[key_id, key] : keys) {
-                req.mac[key_id] = sas->calculate_mac(key, info + key_id);
-
-                if (!first)
-                        key_list += ",";
-                key_list += key_id;
-                first = false;
-        }
-
-        req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
-
-        return req;
-}
-
-//! sends the mac of the keys
-void
-DeviceVerificationFlow::sendVerificationMac()
-{
-        std::map key_list;
-        key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
-
-        // send our master key, if we trust it
-        if (!this->our_trusted_master_key.empty())
-                key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
-
-        mtx::events::msg::KeyVerificationMac req =
-          key_verification_mac(sas.get(),
-                               http::client()->user_id(),
-                               http::client()->device_id(),
-                               this->toClient,
-                               this->deviceId.toStdString(),
-                               this->transaction_id,
-                               key_list);
-
-        send(req);
-
-        setState(WaitingForMac);
-        acceptDevice();
-}
-//! Completes the verification flow
-void
-DeviceVerificationFlow::acceptDevice()
-{
-        if (!isMacVerified) {
-                setState(WaitingForMac);
-        } else if (state_ == WaitingForMac) {
-                cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
-                this->sendVerificationDone();
-                setState(Success);
-
-                // Request secrets. We should probably check somehow, if a device knowns about the
-                // secrets.
-                if (utils::localUser().toStdString() == this->toClient.to_string() &&
-                    (!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) ||
-                     !cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) {
-                        olm::request_cross_signing_keys();
-                }
-        }
-}
-
-void
-DeviceVerificationFlow::unverify()
-{
-        cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
-
-        emit refreshProfile();
-}
-
-QSharedPointer
-DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
-                                              TimelineModel *timelineModel_,
-                                              const mtx::events::msg::KeyVerificationRequest &msg,
-                                              QString other_user_,
-                                              QString event_id_)
-{
-        QSharedPointer flow(
-          new DeviceVerificationFlow(parent_,
-                                     Type::RoomMsg,
-                                     timelineModel_,
-                                     other_user_,
-                                     QString::fromStdString(msg.from_device)));
-
-        flow->setEventId(event_id_.toStdString());
-
-        if (std::find(msg.methods.begin(),
-                      msg.methods.end(),
-                      mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
-                flow->cancelVerification(UnknownMethod);
-        }
-
-        return flow;
-}
-QSharedPointer
-DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
-                                                const mtx::events::msg::KeyVerificationRequest &msg,
-                                                QString other_user_,
-                                                QString txn_id_)
-{
-        QSharedPointer flow(new DeviceVerificationFlow(
-          parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
-        flow->transaction_id = txn_id_.toStdString();
-
-        if (std::find(msg.methods.begin(),
-                      msg.methods.end(),
-                      mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
-                flow->cancelVerification(UnknownMethod);
-        }
-
-        return flow;
-}
-QSharedPointer
-DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
-                                                const mtx::events::msg::KeyVerificationStart &msg,
-                                                QString other_user_,
-                                                QString txn_id_)
-{
-        QSharedPointer flow(new DeviceVerificationFlow(
-          parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
-        flow->transaction_id = txn_id_.toStdString();
-
-        flow->handleStartMessage(msg, "");
-
-        return flow;
-}
-QSharedPointer
-DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
-                                                 TimelineModel *timelineModel_,
-                                                 QString userid)
-{
-        QSharedPointer flow(
-          new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
-        flow->sender = true;
-        return flow;
-}
-QSharedPointer
-DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
-{
-        QSharedPointer flow(
-          new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
-
-        flow->sender         = true;
-        flow->transaction_id = http::client()->generate_txn_id();
-
-        return flow;
-}
diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h
deleted file mode 100644
index 4685a450..00000000
--- a/src/DeviceVerificationFlow.h
+++ /dev/null
@@ -1,251 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include 
-
-#include 
-#include 
-
-#include "CacheCryptoStructs.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Olm.h"
-#include "timeline/TimelineModel.h"
-
-class QTimer;
-
-using sas_ptr = std::unique_ptr;
-
-// clang-format off
-/*
- * Stolen from fluffy chat :D
- *
- *      State         |   +-------------+                    +-----------+                                  |
- *                    |   | AliceDevice |                    | BobDevice |                                  |
- *                    |   | (sender)    |                    |           |                                  |
- *                    |   +-------------+                    +-----------+                                  |
- * promptStartVerify  |         |                                 |                                         |
- *                    |      o  | (m.key.verification.request)    |                                         |
- *                    |      p  |-------------------------------->| (ASK FOR VERIFICATION REQUEST)          |
- * waitForOtherAccept |      t  |                                 |                                         | promptStartVerify
- * &&                 |      i  |      (m.key.verification.ready) |                                         |
- * no commitment      |      o  |<--------------------------------|                                         |
- * &&                 |      n  |                                 |                                         |
- * no canonical_json  |      a  |      (m.key.verification.start) |                                         | waitingForKeys
- *                    |      l  |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
- *                    |         |                                 |                                         | && no canonical_json
- *                    |         | m.key.verification.start        |                                         |
- * waitForOtherAccept |         |-------------------------------->| (IF NOT ALREADY ASKED,                  |
- * &&                 |         |                                 |  ASK FOR VERIFICATION REQUEST)          | promptStartVerify, if not accepted
- * canonical_json     |         |       m.key.verification.accept |                                         |
- *                    |         |<--------------------------------|                                         |
- * waitForOtherAccept |         |                                 |                                         | waitingForKeys
- * &&                 |         | m.key.verification.key          |                                         | && canonical_json
- * commitment         |         |-------------------------------->|                                         | && commitment
- *                    |         |                                 |                                         |
- *                    |         |          m.key.verification.key |                                         |
- *                    |         |<--------------------------------|                                         |
- * compareEmoji/Number|         |                                 |                                         | compareEmoji/Number
- *                    |         |     COMPARE EMOJI / NUMBERS     |                                         |
- *                    |         |                                 |                                         |
- * waitingForMac      |         |     m.key.verification.mac      |                                         | waitingForMac
- *                    | success |<------------------------------->|  success                                |
- *                    |         |                                 |                                         |
- * success/fail       |         |         m.key.verification.done |                                         | success/fail
- *                    |         |<------------------------------->|                                         |
- */
-// clang-format on
-class DeviceVerificationFlow : public QObject
-{
-        Q_OBJECT
-        Q_PROPERTY(QString state READ state NOTIFY stateChanged)
-        Q_PROPERTY(Error error READ error NOTIFY errorChanged)
-        Q_PROPERTY(QString userId READ getUserId CONSTANT)
-        Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
-        Q_PROPERTY(bool sender READ getSender CONSTANT)
-        Q_PROPERTY(std::vector sasList READ getSasList CONSTANT)
-        Q_PROPERTY(bool isDeviceVerification READ isDeviceVerification CONSTANT)
-        Q_PROPERTY(bool isSelfVerification READ isSelfVerification CONSTANT)
-
-public:
-        enum State
-        {
-                PromptStartVerification,
-                WaitingForOtherToAccept,
-                WaitingForKeys,
-                CompareEmoji,
-                CompareNumber,
-                WaitingForMac,
-                Success,
-                Failed,
-        };
-        Q_ENUM(State)
-
-        enum Type
-        {
-                ToDevice,
-                RoomMsg
-        };
-
-        enum Error
-        {
-                UnknownMethod,
-                MismatchedCommitment,
-                MismatchedSAS,
-                KeyMismatch,
-                Timeout,
-                User,
-                OutOfOrder,
-        };
-        Q_ENUM(Error)
-
-        static QSharedPointer NewInRoomVerification(
-          QObject *parent_,
-          TimelineModel *timelineModel_,
-          const mtx::events::msg::KeyVerificationRequest &msg,
-          QString other_user_,
-          QString event_id_);
-        static QSharedPointer NewToDeviceVerification(
-          QObject *parent_,
-          const mtx::events::msg::KeyVerificationRequest &msg,
-          QString other_user_,
-          QString txn_id_);
-        static QSharedPointer NewToDeviceVerification(
-          QObject *parent_,
-          const mtx::events::msg::KeyVerificationStart &msg,
-          QString other_user_,
-          QString txn_id_);
-        static QSharedPointer
-        InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
-        static QSharedPointer InitiateDeviceVerification(QObject *parent,
-                                                                                 QString userid,
-                                                                                 QString device);
-
-        // getters
-        QString state();
-        Error error() { return error_; }
-        QString getUserId();
-        QString getDeviceId();
-        bool getSender();
-        std::vector getSasList();
-        QString transactionId() { return QString::fromStdString(this->transaction_id); }
-        // setters
-        void setDeviceId(QString deviceID);
-        void setEventId(std::string event_id);
-        bool isDeviceVerification() const
-        {
-                return this->type == DeviceVerificationFlow::Type::ToDevice;
-        }
-        bool isSelfVerification() const;
-
-        void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
-
-public slots:
-        //! unverifies a device
-        void unverify();
-        //! Continues the flow
-        void next();
-        //! Cancel the flow
-        void cancel() { cancelVerification(User); }
-
-signals:
-        void refreshProfile();
-        void stateChanged();
-        void errorChanged();
-
-private:
-        DeviceVerificationFlow(QObject *,
-                               DeviceVerificationFlow::Type flow_type,
-                               TimelineModel *model,
-                               QString userID,
-                               QString deviceId_);
-        void setState(State state)
-        {
-                if (state != state_) {
-                        state_ = state;
-                        emit stateChanged();
-                }
-        }
-
-        void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
-        //! sends a verification request
-        void sendVerificationRequest();
-        //! accepts a verification request
-        void sendVerificationReady();
-        //! completes the verification flow();
-        void sendVerificationDone();
-        //! accepts a verification
-        void acceptVerificationRequest();
-        //! starts the verification flow
-        void startVerificationRequest();
-        //! cancels a verification flow
-        void cancelVerification(DeviceVerificationFlow::Error error_code);
-        //! sends the verification key
-        void sendVerificationKey();
-        //! sends the mac of the keys
-        void sendVerificationMac();
-        //! Completes the verification flow
-        void acceptDevice();
-
-        std::string transaction_id;
-
-        bool sender;
-        Type type;
-        mtx::identifiers::User toClient;
-        QString deviceId;
-
-        // public part of our master key, when trusted or empty
-        std::string our_trusted_master_key;
-
-        mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
-        QTimer *timeout                     = nullptr;
-        sas_ptr sas;
-        std::string mac_method;
-        std::string commitment;
-        nlohmann::json canonical_json;
-
-        std::vector sasList;
-        UserKeyCache their_keys;
-        TimelineModel *model_;
-        mtx::common::Relation relation;
-
-        State state_ = PromptStartVerification;
-        Error error_ = UnknownMethod;
-
-        bool isMacVerified = false;
-
-        template
-        void send(T msg)
-        {
-                if (this->type == DeviceVerificationFlow::Type::ToDevice) {
-                        mtx::requests::ToDeviceMessages body;
-                        msg.transaction_id                           = this->transaction_id;
-                        body[this->toClient][deviceId.toStdString()] = msg;
-
-                        http::client()->send_to_device(
-                          this->transaction_id, body, [](mtx::http::RequestErr err) {
-                                  if (err)
-                                          nhlog::net()->warn(
-                                            "failed to send verification to_device message: {} {}",
-                                            err->matrix_error.error,
-                                            static_cast(err->status_code));
-                          });
-                } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
-                        if constexpr (!std::is_same_v) {
-                                msg.relations.relations.push_back(this->relation);
-                                // Set synthesized to surpress the nheko relation extensions
-                                msg.relations.synthesized = true;
-                        }
-                        (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type);
-                }
-
-                nhlog::net()->debug(
-                  "Sent verification step: {} in state: {}",
-                  mtx::events::to_string(mtx::events::to_device_content_to_type),
-                  state().toStdString());
-        }
-};
diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index 362bf4e9..d794a384 100644
--- a/src/EventAccessors.cpp
+++ b/src/EventAccessors.cpp
@@ -16,463 +16,460 @@ using is_detected = typename nheko::detail::detector
-        bool operator()(const mtx::events::StateEvent &)
-        {
-                return true;
-        }
-        template
-        bool operator()(const mtx::events::Event &)
-        {
-                return false;
-        }
+    template
+    bool operator()(const mtx::events::StateEvent &)
+    {
+        return true;
+    }
+    template
+    bool operator()(const mtx::events::Event &)
+    {
+        return false;
+    }
 };
 
 struct EventMsgType
 {
-        template
-        using msgtype_t = decltype(E::msgtype);
-        template
-        mtx::events::MessageType operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        if constexpr (std::is_same_v,
-                                                     std::remove_cv_t>)
-                                return mtx::events::getMessageType(e.content.msgtype.value());
-                        else if constexpr (std::is_same_v<
-                                             std::string,
-                                             std::remove_cv_t>)
-                                return mtx::events::getMessageType(e.content.msgtype);
-                }
-                return mtx::events::MessageType::Unknown;
+    template
+    using msgtype_t = decltype(E::msgtype);
+    template
+    mtx::events::MessageType operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            if constexpr (std::is_same_v,
+                                         std::remove_cv_t>)
+                return mtx::events::getMessageType(e.content.msgtype.value());
+            else if constexpr (std::is_same_v>)
+                return mtx::events::getMessageType(e.content.msgtype);
         }
+        return mtx::events::MessageType::Unknown;
+    }
 };
 
 struct EventRoomName
 {
-        template
-        std::string operator()(const T &e)
-        {
-                if constexpr (std::is_same_v, T>)
-                        return e.content.name;
-                return "";
-        }
+    template
+    std::string operator()(const T &e)
+    {
+        if constexpr (std::is_same_v, T>)
+            return e.content.name;
+        return "";
+    }
 };
 
 struct EventRoomTopic
 {
-        template
-        std::string operator()(const T &e)
-        {
-                if constexpr (std::is_same_v, T>)
-                        return e.content.topic;
-                return "";
-        }
+    template
+    std::string operator()(const T &e)
+    {
+        if constexpr (std::is_same_v, T>)
+            return e.content.topic;
+        return "";
+    }
 };
 
 struct CallType
 {
-        template
-        std::string operator()(const T &e)
-        {
-                if constexpr (std::is_same_v,
-                                             T>) {
-                        const char video[]     = "m=video";
-                        const std::string &sdp = e.content.sdp;
-                        return 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()
-                                 ? "video"
-                                 : "voice";
-                }
-                return std::string();
+    template
+    std::string operator()(const T &e)
+    {
+        if constexpr (std::is_same_v, T>) {
+            const char video[]     = "m=video";
+            const std::string &sdp = e.content.sdp;
+            return 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()
+                     ? "video"
+                     : "voice";
         }
+        return std::string();
+    }
 };
 
 struct EventBody
 {
-        template
-        using body_t = decltype(C::body);
-        template
-        std::string operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        if constexpr (std::is_same_v,
-                                                     std::remove_cv_t>)
-                                return e.content.body ? e.content.body.value() : "";
-                        else if constexpr (std::is_same_v<
-                                             std::string,
-                                             std::remove_cv_t>)
-                                return e.content.body;
-                }
-                return "";
+    template
+    using body_t = decltype(C::body);
+    template
+    std::string operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            if constexpr (std::is_same_v,
+                                         std::remove_cv_t>)
+                return e.content.body ? e.content.body.value() : "";
+            else if constexpr (std::is_same_v>)
+                return e.content.body;
         }
+        return "";
+    }
 };
 
 struct EventFormattedBody
 {
-        template
-        using formatted_body_t = decltype(C::formatted_body);
-        template
-        std::string operator()(const mtx::events::RoomEvent &e)
-        {
-                if constexpr (is_detected::value) {
-                        if (e.content.format == "org.matrix.custom.html")
-                                return e.content.formatted_body;
-                }
-                return "";
+    template
+    using formatted_body_t = decltype(C::formatted_body);
+    template
+    std::string operator()(const mtx::events::RoomEvent &e)
+    {
+        if constexpr (is_detected::value) {
+            if (e.content.format == "org.matrix.custom.html")
+                return e.content.formatted_body;
         }
+        return "";
+    }
 };
 
 struct EventFile
 {
-        template
-        using file_t = decltype(Content::file);
-        template
-        std::optional operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value)
-                        return e.content.file;
-                return std::nullopt;
-        }
+    template
+    using file_t = decltype(Content::file);
+    template
+    std::optional operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value)
+            return e.content.file;
+        return std::nullopt;
+    }
 };
 
 struct EventUrl
 {
-        template
-        using url_t = decltype(Content::url);
-        template
-        std::string operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        if (auto file = EventFile{}(e))
-                                return file->url;
-                        return e.content.url;
-                }
-                return "";
+    template
+    using url_t = decltype(Content::url);
+    template
+    std::string operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            if (auto file = EventFile{}(e))
+                return file->url;
+            return e.content.url;
         }
+        return "";
+    }
 };
 
 struct EventThumbnailUrl
 {
-        template
-        using thumbnail_url_t = decltype(Content::info.thumbnail_url);
-        template
-        std::string operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        return e.content.info.thumbnail_url;
-                }
-                return "";
+    template
+    using thumbnail_url_t = decltype(Content::info.thumbnail_url);
+    template
+    std::string operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            return e.content.info.thumbnail_url;
         }
+        return "";
+    }
 };
 
 struct EventBlurhash
 {
-        template
-        using blurhash_t = decltype(Content::info.blurhash);
-        template
-        std::string operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        return e.content.info.blurhash;
-                }
-                return "";
+    template
+    using blurhash_t = decltype(Content::info.blurhash);
+    template
+    std::string operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            return e.content.info.blurhash;
         }
+        return "";
+    }
 };
 
 struct EventFilename
 {
-        template
-        std::string operator()(const mtx::events::Event &)
-        {
-                return "";
-        }
-        std::string operator()(const mtx::events::RoomEvent &e)
-        {
-                // body may be the original filename
-                return e.content.body;
-        }
-        std::string operator()(const mtx::events::RoomEvent &e)
-        {
-                // body may be the original filename
-                return e.content.body;
-        }
-        std::string operator()(const mtx::events::RoomEvent &e)
-        {
-                // body may be the original filename
-                return e.content.body;
-        }
-        std::string operator()(const mtx::events::RoomEvent &e)
-        {
-                // body may be the original filename
-                if (!e.content.filename.empty())
-                        return e.content.filename;
-                return e.content.body;
-        }
+    template
+    std::string operator()(const mtx::events::Event &)
+    {
+        return "";
+    }
+    std::string operator()(const mtx::events::RoomEvent &e)
+    {
+        // body may be the original filename
+        return e.content.body;
+    }
+    std::string operator()(const mtx::events::RoomEvent &e)
+    {
+        // body may be the original filename
+        return e.content.body;
+    }
+    std::string operator()(const mtx::events::RoomEvent &e)
+    {
+        // body may be the original filename
+        return e.content.body;
+    }
+    std::string operator()(const mtx::events::RoomEvent &e)
+    {
+        // body may be the original filename
+        if (!e.content.filename.empty())
+            return e.content.filename;
+        return e.content.body;
+    }
 };
 
 struct EventMimeType
 {
-        template
-        using mimetype_t = decltype(Content::info.mimetype);
-        template
-        std::string operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        return e.content.info.mimetype;
-                }
-                return "";
+    template
+    using mimetype_t = decltype(Content::info.mimetype);
+    template
+    std::string operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            return e.content.info.mimetype;
         }
+        return "";
+    }
 };
 
 struct EventFilesize
 {
-        template
-        using filesize_t = decltype(Content::info.size);
-        template
-        int64_t operator()(const mtx::events::RoomEvent &e)
-        {
-                if constexpr (is_detected::value) {
-                        return e.content.info.size;
-                }
-                return 0;
+    template
+    using filesize_t = decltype(Content::info.size);
+    template
+    int64_t operator()(const mtx::events::RoomEvent &e)
+    {
+        if constexpr (is_detected::value) {
+            return e.content.info.size;
         }
+        return 0;
+    }
 };
 
 struct EventRelations
 {
-        template
-        using related_ev_id_t = decltype(Content::relations);
-        template
-        mtx::common::Relations operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        return e.content.relations;
-                }
-                return {};
+    template
+    using related_ev_id_t = decltype(Content::relations);
+    template
+    mtx::common::Relations operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            return e.content.relations;
         }
+        return {};
+    }
 };
 
 struct SetEventRelations
 {
-        mtx::common::Relations new_relations;
-        template
-        using related_ev_id_t = decltype(Content::relations);
-        template
-        void operator()(mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        e.content.relations = std::move(new_relations);
-                }
+    mtx::common::Relations new_relations;
+    template
+    using related_ev_id_t = decltype(Content::relations);
+    template
+    void operator()(mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            e.content.relations = std::move(new_relations);
         }
+    }
 };
 
 struct EventTransactionId
 {
-        template
-        std::string operator()(const mtx::events::RoomEvent &e)
-        {
-                return e.unsigned_data.transaction_id;
-        }
-        template
-        std::string operator()(const mtx::events::Event &e)
-        {
-                return e.unsigned_data.transaction_id;
-        }
+    template
+    std::string operator()(const mtx::events::RoomEvent &e)
+    {
+        return e.unsigned_data.transaction_id;
+    }
+    template
+    std::string operator()(const mtx::events::Event &e)
+    {
+        return e.unsigned_data.transaction_id;
+    }
 };
 
 struct EventMediaHeight
 {
-        template
-        using h_t = decltype(Content::info.h);
-        template
-        uint64_t operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        return e.content.info.h;
-                }
-                return -1;
+    template
+    using h_t = decltype(Content::info.h);
+    template
+    uint64_t operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            return e.content.info.h;
         }
+        return -1;
+    }
 };
 
 struct EventMediaWidth
 {
-        template
-        using w_t = decltype(Content::info.w);
-        template
-        uint64_t operator()(const mtx::events::Event &e)
-        {
-                if constexpr (is_detected::value) {
-                        return e.content.info.w;
-                }
-                return -1;
+    template
+    using w_t = decltype(Content::info.w);
+    template
+    uint64_t operator()(const mtx::events::Event &e)
+    {
+        if constexpr (is_detected::value) {
+            return e.content.info.w;
         }
+        return -1;
+    }
 };
 
 template
 double
 eventPropHeight(const mtx::events::RoomEvent &e)
 {
-        auto w = eventWidth(e);
-        if (w == 0)
-                w = 1;
+    auto w = eventWidth(e);
+    if (w == 0)
+        w = 1;
 
-        double prop = eventHeight(e) / (double)w;
+    double prop = eventHeight(e) / (double)w;
 
-        return prop > 0 ? prop : 1.;
+    return prop > 0 ? prop : 1.;
 }
 }
 
 std::string
 mtx::accessors::event_id(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto e) { return e.event_id; }, event);
+    return std::visit([](const auto e) { return e.event_id; }, event);
 }
 std::string
 mtx::accessors::room_id(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto e) { return e.room_id; }, event);
+    return std::visit([](const auto e) { return e.room_id; }, event);
 }
 
 std::string
 mtx::accessors::sender(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit([](const auto e) { return e.sender; }, event);
+    return std::visit([](const auto e) { return e.sender; }, event);
 }
 
 QDateTime
 mtx::accessors::origin_server_ts(const mtx::events::collections::TimelineEvents &event)
 {
-        return QDateTime::fromMSecsSinceEpoch(
-          std::visit([](const auto e) { return e.origin_server_ts; }, event));
+    return QDateTime::fromMSecsSinceEpoch(
+      std::visit([](const auto e) { return e.origin_server_ts; }, event));
 }
 
 std::string
 mtx::accessors::filename(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventFilename{}, event);
+    return std::visit(EventFilename{}, event);
 }
 
 mtx::events::MessageType
 mtx::accessors::msg_type(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventMsgType{}, event);
+    return std::visit(EventMsgType{}, event);
 }
 std::string
 mtx::accessors::room_name(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventRoomName{}, event);
+    return std::visit(EventRoomName{}, event);
 }
 std::string
 mtx::accessors::room_topic(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventRoomTopic{}, event);
+    return std::visit(EventRoomTopic{}, event);
 }
 
 std::string
 mtx::accessors::call_type(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(CallType{}, event);
+    return std::visit(CallType{}, event);
 }
 
 std::string
 mtx::accessors::body(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventBody{}, event);
+    return std::visit(EventBody{}, event);
 }
 
 std::string
 mtx::accessors::formatted_body(const mtx::events::collections::TimelineEvents &event)
 {
-        return std::visit(EventFormattedBody{}, event);
+    return std::visit(EventFormattedBody{}, event);
 }
 
 QString
 mtx::accessors::formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event)
 {
-        auto formatted = formatted_body(event);
-        if (!formatted.empty())
-                return QString::fromStdString(formatted);
-        else
-                return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "
"); + auto formatted = formatted_body(event); + if (!formatted.empty()) + return QString::fromStdString(formatted); + else + return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "
"); } std::optional mtx::accessors::file(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventFile{}, event); + return std::visit(EventFile{}, event); } std::string mtx::accessors::url(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventUrl{}, event); + return std::visit(EventUrl{}, event); } std::string mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventThumbnailUrl{}, event); + return std::visit(EventThumbnailUrl{}, event); } std::string mtx::accessors::blurhash(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventBlurhash{}, event); + return std::visit(EventBlurhash{}, event); } std::string mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventMimeType{}, event); + return std::visit(EventMimeType{}, event); } mtx::common::Relations mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventRelations{}, event); + return std::visit(EventRelations{}, event); } void mtx::accessors::set_relations(mtx::events::collections::TimelineEvents &event, mtx::common::Relations relations) { - std::visit(SetEventRelations{std::move(relations)}, event); + std::visit(SetEventRelations{std::move(relations)}, event); } std::string mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventTransactionId{}, event); + return std::visit(EventTransactionId{}, event); } int64_t mtx::accessors::filesize(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventFilesize{}, event); + return std::visit(EventFilesize{}, event); } uint64_t mtx::accessors::media_height(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventMediaHeight{}, event); + return std::visit(EventMediaHeight{}, event); } uint64_t mtx::accessors::media_width(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventMediaWidth{}, event); + return std::visit(EventMediaWidth{}, event); } nlohmann::json mtx::accessors::serialize_event(const mtx::events::collections::TimelineEvents &event) { - return std::visit([](const auto &e) { return nlohmann::json(e); }, event); + return std::visit([](const auto &e) { return nlohmann::json(e); }, event); } bool mtx::accessors::is_state_event(const mtx::events::collections::TimelineEvents &event) { - return std::visit(IsStateEvent{}, event); + return std::visit(IsStateEvent{}, event); } diff --git a/src/EventAccessors.h b/src/EventAccessors.h index a58c7de0..c6b8e854 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -14,24 +14,24 @@ namespace nheko { struct nonesuch { - ~nonesuch() = delete; - nonesuch(nonesuch const &) = delete; - void operator=(nonesuch const &) = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const &) = delete; + void operator=(nonesuch const &) = delete; }; namespace detail { template class Op, class... Args> struct detector { - using value_t = std::false_type; - using type = Default; + using value_t = std::false_type; + using type = Default; }; template class Op, class... Args> struct detector>, Op, Args...> { - using value_t = std::true_type; - using type = Op; + using value_t = std::true_type; + using type = Op; }; } // namespace detail diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp index 6392de22..39e46f01 100644 --- a/src/ImagePackListModel.cpp +++ b/src/ImagePackListModel.cpp @@ -13,82 +13,81 @@ ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *paren : QAbstractListModel(parent) , room_id(roomId) { - auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt); + auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt); - for (const auto &pack : packs_) { - packs.push_back( - QSharedPointer(new SingleImagePackModel(pack))); - } + for (const auto &pack : packs_) { + packs.push_back(QSharedPointer(new SingleImagePackModel(pack))); + } } int ImagePackListModel::rowCount(const QModelIndex &) const { - return (int)packs.size(); + return (int)packs.size(); } QHash ImagePackListModel::roleNames() const { - return { - {Roles::DisplayName, "displayName"}, - {Roles::AvatarUrl, "avatarUrl"}, - {Roles::FromAccountData, "fromAccountData"}, - {Roles::FromCurrentRoom, "fromCurrentRoom"}, - {Roles::StateKey, "statekey"}, - {Roles::RoomId, "roomid"}, - }; + return { + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::FromAccountData, "fromAccountData"}, + {Roles::FromCurrentRoom, "fromCurrentRoom"}, + {Roles::StateKey, "statekey"}, + {Roles::RoomId, "roomid"}, + }; } QVariant ImagePackListModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - const auto &pack = packs.at(index.row()); - switch (role) { - case Roles::DisplayName: - return pack->packname(); - case Roles::AvatarUrl: - return pack->avatarUrl(); - case Roles::FromAccountData: - return pack->roomid().isEmpty(); - case Roles::FromCurrentRoom: - return pack->roomid().toStdString() == this->room_id; - case Roles::StateKey: - return pack->statekey(); - case Roles::RoomId: - return pack->roomid(); - default: - return {}; - } + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &pack = packs.at(index.row()); + switch (role) { + case Roles::DisplayName: + return pack->packname(); + case Roles::AvatarUrl: + return pack->avatarUrl(); + case Roles::FromAccountData: + return pack->roomid().isEmpty(); + case Roles::FromCurrentRoom: + return pack->roomid().toStdString() == this->room_id; + case Roles::StateKey: + return pack->statekey(); + case Roles::RoomId: + return pack->roomid(); + default: + return {}; } - return {}; + } + return {}; } SingleImagePackModel * ImagePackListModel::packAt(int row) { - if (row < 0 || static_cast(row) >= packs.size()) - return {}; - auto e = packs.at(row).get(); - QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership); - return e; + if (row < 0 || static_cast(row) >= packs.size()) + return {}; + auto e = packs.at(row).get(); + QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership); + return e; } SingleImagePackModel * ImagePackListModel::newPack(bool inRoom) { - ImagePackInfo info{}; - if (inRoom) - info.source_room = room_id; - return new SingleImagePackModel(info); + ImagePackInfo info{}; + if (inRoom) + info.source_room = room_id; + return new SingleImagePackModel(info); } bool ImagePackListModel::containsAccountPack() const { - for (const auto &p : packs) - if (p->roomid().isEmpty()) - return true; - return false; + for (const auto &p : packs) + if (p->roomid().isEmpty()) + return true; + return false; } diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h index 2aa5abb2..0b39729a 100644 --- a/src/ImagePackListModel.h +++ b/src/ImagePackListModel.h @@ -11,31 +11,31 @@ class SingleImagePackModel; class ImagePackListModel : public QAbstractListModel { - Q_OBJECT - Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT) + Q_OBJECT + Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT) public: - enum Roles - { - DisplayName = Qt::UserRole, - AvatarUrl, - FromAccountData, - FromCurrentRoom, - StateKey, - RoomId, - }; + enum Roles + { + DisplayName = Qt::UserRole, + AvatarUrl, + FromAccountData, + FromCurrentRoom, + StateKey, + RoomId, + }; - ImagePackListModel(const std::string &roomId, QObject *parent = nullptr); - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; + ImagePackListModel(const std::string &roomId, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; - Q_INVOKABLE SingleImagePackModel *packAt(int row); - Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom); + Q_INVOKABLE SingleImagePackModel *packAt(int row); + Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom); - bool containsAccountPack() const; + bool containsAccountPack() const; private: - std::string room_id; + std::string room_id; - std::vector> packs; + std::vector> packs; }; diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 27b2116f..e045581a 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -16,69 +16,68 @@ InviteesModel::InviteesModel(QObject *parent) void InviteesModel::addUser(QString mxid) { - beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); + beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); - auto invitee = new Invitee{mxid, this}; - auto indexOfInvitee = invitees_.count(); - connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { - emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); - }); + auto invitee = new Invitee{mxid, this}; + auto indexOfInvitee = invitees_.count(); + connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { + emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); + }); - invitees_.push_back(invitee); + invitees_.push_back(invitee); - endInsertRows(); - emit countChanged(); + endInsertRows(); + emit countChanged(); } QHash InviteesModel::roleNames() const { - return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; + return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; } QVariant InviteesModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0) - return {}; + if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0) + return {}; - switch (role) { - case Mxid: - return invitees_[index.row()]->mxid_; - case DisplayName: - return invitees_[index.row()]->displayName_; - case AvatarUrl: - return invitees_[index.row()]->avatarUrl_; - default: - return {}; - } + switch (role) { + case Mxid: + return invitees_[index.row()]->mxid_; + case DisplayName: + return invitees_[index.row()]->displayName_; + case AvatarUrl: + return invitees_[index.row()]->avatarUrl_; + default: + return {}; + } } QStringList InviteesModel::mxids() { - QStringList mxidList; - for (int i = 0; i < invitees_.length(); ++i) - mxidList.push_back(invitees_[i]->mxid_); - return mxidList; + QStringList mxidList; + for (int i = 0; i < invitees_.length(); ++i) + mxidList.push_back(invitees_[i]->mxid_); + return mxidList; } Invitee::Invitee(const QString &mxid, QObject *parent) : QObject{parent} , mxid_{mxid} { - http::client()->get_profile( - mxid_.toStdString(), - [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve profile info"); - emit userInfoLoaded(); - return; - } + http::client()->get_profile( + mxid_.toStdString(), [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve profile info"); + emit userInfoLoaded(); + return; + } - displayName_ = QString::fromStdString(res.display_name); - avatarUrl_ = QString::fromStdString(res.avatar_url); + displayName_ = QString::fromStdString(res.display_name); + avatarUrl_ = QString::fromStdString(res.avatar_url); - emit userInfoLoaded(); - }); + emit userInfoLoaded(); + }); } diff --git a/src/InviteesModel.h b/src/InviteesModel.h index a4e19ebb..fd64116b 100644 --- a/src/InviteesModel.h +++ b/src/InviteesModel.h @@ -10,54 +10,54 @@ class Invitee : public QObject { - Q_OBJECT + Q_OBJECT public: - Invitee(const QString &mxid, QObject *parent = nullptr); + Invitee(const QString &mxid, QObject *parent = nullptr); signals: - void userInfoLoaded(); + void userInfoLoaded(); private: - const QString mxid_; - QString displayName_; - QString avatarUrl_; + const QString mxid_; + QString displayName_; + QString avatarUrl_; - friend class InviteesModel; + friend class InviteesModel; }; class InviteesModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: - enum Roles - { - Mxid, - DisplayName, - AvatarUrl, - }; + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + }; - InviteesModel(QObject *parent = nullptr); + InviteesModel(QObject *parent = nullptr); - Q_INVOKABLE void addUser(QString mxid); + Q_INVOKABLE void addUser(QString mxid); - QHash roleNames() const override; - int rowCount(const QModelIndex & = QModelIndex()) const override - { - return (int)invitees_.size(); - } - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QStringList mxids(); + QHash roleNames() const override; + int rowCount(const QModelIndex & = QModelIndex()) const override + { + return (int)invitees_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QStringList mxids(); signals: - void accept(); - void countChanged(); + void accept(); + void countChanged(); private: - QVector invitees_; + QVector invitees_; }; #endif // INVITEESMODEL_H diff --git a/src/JdenticonProvider.cpp b/src/JdenticonProvider.cpp new file mode 100644 index 00000000..e2828286 --- /dev/null +++ b/src/JdenticonProvider.cpp @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "JdenticonProvider.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "Utils.h" +#include "jdenticoninterface.h" + +static QPixmap +clipRadius(QPixmap img, double radius) +{ + QPixmap out(img.size()); + out.fill(Qt::transparent); + + QPainter painter(&out); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + QPainterPath ppath; + ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); + + painter.setClipPath(ppath); + painter.drawPixmap(img.rect(), img); + + return out; +} + +JdenticonResponse::JdenticonResponse(const QString &key, + bool crop, + double radius, + const QSize &requestedSize) + : m_key(key) + , m_crop{crop} + , m_radius{radius} + , m_requestedSize(requestedSize.isValid() ? requestedSize : QSize(100, 100)) + , m_pixmap{m_requestedSize} + , jdenticonInterface_{Jdenticon::getJdenticonInterface()} +{ + setAutoDelete(false); +} + +void +JdenticonResponse::run() +{ + m_pixmap.fill(Qt::transparent); + + QPainter painter; + painter.begin(&m_pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + try { + QSvgRenderer renderer{ + jdenticonInterface_->generate(m_key, m_requestedSize.width()).toUtf8()}; + renderer.render(&painter); + } catch (std::exception &e) { + nhlog::ui()->error( + "caught {} in jdenticonprovider, key '{}'", e.what(), m_key.toStdString()); + } + + painter.end(); + + m_pixmap = clipRadius(m_pixmap, m_radius); + + emit finished(); +} + +namespace Jdenticon { +JdenticonInterface * +getJdenticonInterface() +{ + static JdenticonInterface *interface = nullptr; + static bool interfaceExists{true}; + + if (interface == nullptr && interfaceExists) { + QDir pluginsDir(qApp->applicationDirPath()); + + bool plugins = pluginsDir.cd("plugins"); + if (plugins) { + for (const QString &fileName : pluginsDir.entryList(QDir::Files)) { + QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName)); + QObject *plugin = pluginLoader.instance(); + if (plugin) { + interface = qobject_cast(plugin); + if (interface) { + nhlog::ui()->info("Loaded jdenticon plugin."); + break; + } + } + } + } else { + nhlog::ui()->info("jdenticon plugin not found."); + interfaceExists = false; + } + } + + return interface; +} +} diff --git a/src/JdenticonProvider.h b/src/JdenticonProvider.h new file mode 100644 index 00000000..f4ef6d10 --- /dev/null +++ b/src/JdenticonProvider.h @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include "jdenticoninterface.h" + +namespace Jdenticon { +JdenticonInterface * +getJdenticonInterface(); +} + +class JdenticonResponse + : public QQuickImageResponse + , public QRunnable +{ +public: + JdenticonResponse(const QString &key, bool crop, double radius, const QSize &requestedSize); + + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_pixmap.toImage()); + } + + void run() override; + + QString m_key; + bool m_crop; + double m_radius; + QSize m_requestedSize; + QPixmap m_pixmap; + JdenticonInterface *jdenticonInterface_ = nullptr; +}; + +class JdenticonProvider + : public QObject + , public QQuickAsyncImageProvider +{ + Q_OBJECT + +public: + static bool isAvailable() { return Jdenticon::getJdenticonInterface() != nullptr; } + +public slots: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + auto id_ = id; + bool crop = true; + double radius = 0; + + auto queryStart = id.lastIndexOf('?'); + if (queryStart != -1) { + id_ = id.left(queryStart); + auto query = id.midRef(queryStart + 1); + auto queryBits = query.split('&'); + + for (auto b : queryBits) { + if (b.startsWith("radius=")) { + radius = b.mid(7).toDouble(); + } + } + } + + JdenticonResponse *response = new JdenticonResponse(id_, crop, radius, requestedSize); + pool.start(response); + return response; + } + +private: + QThreadPool pool; +}; diff --git a/src/Logging.cpp b/src/Logging.cpp index 67bcaf7a..a18a1cee 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -25,35 +25,35 @@ constexpr auto MAX_LOG_FILES = 3; void qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - std::string localMsg = msg.toStdString(); - const char *file = context.file ? context.file : ""; - const char *function = context.function ? context.function : ""; + std::string localMsg = msg.toStdString(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; - if ( - // The default style has the point size set. If you use pixel size anywhere, you get - // that warning, which is useless, since sometimes you need the pixel size to match the - // text to the size of the outer element for example. This is done in the avatar and - // without that you get one warning for every Avatar displayed, which is stupid! - msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size."))) - return; + if ( + // The default style has the point size set. If you use pixel size anywhere, you get + // that warning, which is useless, since sometimes you need the pixel size to match the + // text to the size of the outer element for example. This is done in the avatar and + // without that you get one warning for every Avatar displayed, which is stupid! + msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size."))) + return; - switch (type) { - case QtDebugMsg: - nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtInfoMsg: - nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtWarningMsg: - nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtCriticalMsg: - nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtFatalMsg: - nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - } + switch (type) { + case QtDebugMsg: + nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtInfoMsg: + nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtWarningMsg: + nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtCriticalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtFatalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + } } } @@ -63,60 +63,59 @@ bool enable_debug_log_from_commandline = false; void init(const std::string &file_path) { - auto file_sink = std::make_shared( - file_path, MAX_FILE_SIZE, MAX_LOG_FILES); + auto file_sink = std::make_shared( + file_path, MAX_FILE_SIZE, MAX_LOG_FILES); - auto console_sink = std::make_shared(); + auto console_sink = std::make_shared(); - std::vector sinks; - sinks.push_back(file_sink); - sinks.push_back(console_sink); + std::vector sinks; + sinks.push_back(file_sink); + sinks.push_back(console_sink); - net_logger = std::make_shared("net", std::begin(sinks), std::end(sinks)); - ui_logger = std::make_shared("ui", std::begin(sinks), std::end(sinks)); - db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); - crypto_logger = - std::make_shared("crypto", std::begin(sinks), std::end(sinks)); - qml_logger = std::make_shared("qml", std::begin(sinks), std::end(sinks)); + net_logger = std::make_shared("net", std::begin(sinks), std::end(sinks)); + ui_logger = std::make_shared("ui", std::begin(sinks), std::end(sinks)); + db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); + crypto_logger = std::make_shared("crypto", std::begin(sinks), std::end(sinks)); + qml_logger = std::make_shared("qml", std::begin(sinks), std::end(sinks)); - if (nheko::enable_debug_log || enable_debug_log_from_commandline) { - db_logger->set_level(spdlog::level::trace); - ui_logger->set_level(spdlog::level::trace); - crypto_logger->set_level(spdlog::level::trace); - net_logger->set_level(spdlog::level::trace); - qml_logger->set_level(spdlog::level::trace); - } + if (nheko::enable_debug_log || enable_debug_log_from_commandline) { + db_logger->set_level(spdlog::level::trace); + ui_logger->set_level(spdlog::level::trace); + crypto_logger->set_level(spdlog::level::trace); + net_logger->set_level(spdlog::level::trace); + qml_logger->set_level(spdlog::level::trace); + } - qInstallMessageHandler(qmlMessageHandler); + qInstallMessageHandler(qmlMessageHandler); } std::shared_ptr ui() { - return ui_logger; + return ui_logger; } std::shared_ptr net() { - return net_logger; + return net_logger; } std::shared_ptr db() { - return db_logger; + return db_logger; } std::shared_ptr crypto() { - return crypto_logger; + return crypto_logger; } std::shared_ptr qml() { - return qml_logger; + return qml_logger; } } diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp index f53d81ba..64e9c865 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp @@ -34,477 +34,468 @@ LoginPage::LoginPage(QWidget *parent) : QWidget(parent) , inferredServerAddress_() { - qRegisterMetaType("LoginPage::LoginMethod"); + qRegisterMetaType("LoginPage::LoginMethod"); - top_layout_ = new QVBoxLayout(); + top_layout_ = new QVBoxLayout(); - top_bar_layout_ = new QHBoxLayout(); - top_bar_layout_->setSpacing(0); - top_bar_layout_->setMargin(0); + top_bar_layout_ = new QHBoxLayout(); + top_bar_layout_->setSpacing(0); + top_bar_layout_->setMargin(0); - back_button_ = new FlatButton(this); - back_button_->setMinimumSize(QSize(30, 30)); + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); - top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - top_bar_layout_->addStretch(1); + top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + top_bar_layout_->addStretch(1); - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - back_button_->setIcon(icon); - back_button_->setIconSize(QSize(32, 32)); + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(32, 32)); - QIcon logo; - logo.addFile(":/logos/login.png"); + QIcon logo; + logo.addFile(":/logos/login.png"); - logo_ = new QLabel(this); - logo_->setPixmap(logo.pixmap(128)); + logo_ = new QLabel(this); + logo_->setPixmap(logo.pixmap(128)); - logo_layout_ = new QHBoxLayout(); - logo_layout_->setContentsMargins(0, 0, 0, 20); - logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + logo_layout_ = new QHBoxLayout(); + logo_layout_->setContentsMargins(0, 0, 0, 20); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); - form_wrapper_ = new QHBoxLayout(); - form_widget_ = new QWidget(); - form_widget_->setMinimumSize(QSize(350, 200)); + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 200)); - form_layout_ = new QVBoxLayout(); - form_layout_->setSpacing(20); - form_layout_->setContentsMargins(0, 0, 0, 30); - form_widget_->setLayout(form_layout_); + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 0, 0, 30); + form_widget_->setLayout(form_layout_); - form_wrapper_->addStretch(1); - form_wrapper_->addWidget(form_widget_); - form_wrapper_->addStretch(1); + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); - matrixid_input_ = new TextField(this); - matrixid_input_->setLabel(tr("Matrix ID")); - matrixid_input_->setRegexp(QRegularExpression("@.+?:.{3,}")); - matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); - matrixid_input_->setToolTip( - tr("Your login name. A mxid should start with @ followed by the user id. After the user " - "id you need to include your server name after a :.\nYou can also put your homeserver " - "address there, if your server doesn't support .well-known lookup.\nExample: " - "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a " - "field to enter the server manually.")); + matrixid_input_ = new TextField(this); + matrixid_input_->setLabel(tr("Matrix ID")); + matrixid_input_->setRegexp(QRegularExpression("@.+?:.{3,}")); + matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); + matrixid_input_->setToolTip( + tr("Your login name. A mxid should start with @ followed by the user id. After the user " + "id you need to include your server name after a :.\nYou can also put your homeserver " + "address there, if your server doesn't support .well-known lookup.\nExample: " + "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a " + "field to enter the server manually.")); - spinner_ = new LoadingIndicator(this); - spinner_->setFixedHeight(40); - spinner_->setFixedWidth(40); - spinner_->hide(); + spinner_ = new LoadingIndicator(this); + spinner_->setFixedHeight(40); + spinner_->setFixedWidth(40); + spinner_->hide(); - errorIcon_ = new QLabel(this); - errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); - errorIcon_->hide(); + errorIcon_ = new QLabel(this); + errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); + errorIcon_->hide(); - matrixidLayout_ = new QHBoxLayout(); - matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); + matrixidLayout_ = new QHBoxLayout(); + matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); - QFont font; + QFont font; - error_matrixid_label_ = new QLabel(this); - error_matrixid_label_->setFont(font); - error_matrixid_label_->setWordWrap(true); + error_matrixid_label_ = new QLabel(this); + error_matrixid_label_->setFont(font); + error_matrixid_label_->setWordWrap(true); - password_input_ = new TextField(this); - password_input_->setLabel(tr("Password")); - password_input_->setEchoMode(QLineEdit::Password); - password_input_->setToolTip(tr("Your password.")); + password_input_ = new TextField(this); + password_input_->setLabel(tr("Password")); + password_input_->setEchoMode(QLineEdit::Password); + password_input_->setToolTip(tr("Your password.")); - deviceName_ = new TextField(this); - deviceName_->setLabel(tr("Device name")); - deviceName_->setToolTip( - tr("A name for this device, which will be shown to others, when verifying your devices. " - "If none is provided a default is used.")); + deviceName_ = new TextField(this); + deviceName_->setLabel(tr("Device name")); + deviceName_->setToolTip( + tr("A name for this device, which will be shown to others, when verifying your devices. " + "If none is provided a default is used.")); - serverInput_ = new TextField(this); - serverInput_->setLabel(tr("Homeserver address")); - serverInput_->setPlaceholderText(tr("server.my:8787")); - serverInput_->setToolTip(tr("The address that can be used to contact you homeservers " - "client API.\nExample: https://server.my:8787")); - serverInput_->hide(); + serverInput_ = new TextField(this); + serverInput_->setLabel(tr("Homeserver address")); + serverInput_->setPlaceholderText(tr("server.my:8787")); + serverInput_->setToolTip(tr("The address that can be used to contact you homeservers " + "client API.\nExample: https://server.my:8787")); + serverInput_->hide(); - serverLayout_ = new QHBoxLayout(); - serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); + serverLayout_ = new QHBoxLayout(); + serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); - form_layout_->addLayout(matrixidLayout_); - form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter); - form_layout_->addWidget(password_input_); - form_layout_->addWidget(deviceName_, Qt::AlignHCenter); - form_layout_->addLayout(serverLayout_); + form_layout_->addLayout(matrixidLayout_); + form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter); + form_layout_->addWidget(password_input_); + form_layout_->addWidget(deviceName_, Qt::AlignHCenter); + form_layout_->addLayout(serverLayout_); - error_matrixid_label_->hide(); + error_matrixid_label_->hide(); - button_layout_ = new QHBoxLayout(); - button_layout_->setSpacing(20); - button_layout_->setContentsMargins(0, 0, 0, 30); + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(20); + button_layout_->setContentsMargins(0, 0, 0, 30); - login_button_ = new RaisedButton(tr("LOGIN"), this); - login_button_->setMinimumSize(150, 65); - login_button_->setFontSize(20); - login_button_->setCornerRadius(3); + login_button_ = new RaisedButton(tr("LOGIN"), this); + login_button_->setMinimumSize(150, 65); + login_button_->setFontSize(20); + login_button_->setCornerRadius(3); - sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this); - sso_login_button_->setMinimumSize(150, 65); - sso_login_button_->setFontSize(20); - sso_login_button_->setCornerRadius(3); - sso_login_button_->setVisible(false); + sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this); + sso_login_button_->setMinimumSize(150, 65); + sso_login_button_->setFontSize(20); + sso_login_button_->setCornerRadius(3); + sso_login_button_->setVisible(false); - button_layout_->addStretch(1); - button_layout_->addWidget(login_button_); - button_layout_->addWidget(sso_login_button_); - button_layout_->addStretch(1); + button_layout_->addStretch(1); + button_layout_->addWidget(login_button_); + button_layout_->addWidget(sso_login_button_); + button_layout_->addStretch(1); - error_label_ = new QLabel(this); - error_label_->setFont(font); - error_label_->setWordWrap(true); + error_label_ = new QLabel(this); + error_label_->setFont(font); + error_label_->setWordWrap(true); - top_layout_->addLayout(top_bar_layout_); - top_layout_->addStretch(1); - top_layout_->addLayout(logo_layout_); - top_layout_->addLayout(form_wrapper_); - top_layout_->addStretch(1); - top_layout_->addLayout(button_layout_); - top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); - top_layout_->addStretch(1); + top_layout_->addLayout(top_bar_layout_); + top_layout_->addStretch(1); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); - setLayout(top_layout_); + setLayout(top_layout_); - connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection); - connect( - this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection); + connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection); + connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection); - connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); - connect(login_button_, &RaisedButton::clicked, this, [this]() { - onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO); - }); - connect(sso_login_button_, &RaisedButton::clicked, this, [this]() { - onLoginButtonClicked(LoginMethod::SSO); - }); - connect(this, - &LoginPage::showErrorMessage, - this, - static_cast(&LoginPage::showError), - Qt::QueuedConnection); - connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); - connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(login_button_, &RaisedButton::clicked, this, [this]() { + onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO); + }); + connect(sso_login_button_, &RaisedButton::clicked, this, [this]() { + onLoginButtonClicked(LoginMethod::SSO); + }); + connect(this, + &LoginPage::showErrorMessage, + this, + static_cast(&LoginPage::showError), + Qt::QueuedConnection); + connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); + connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); } void LoginPage::showError(const QString &msg) { - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - error_label_->setFixedHeight((int)qCeil(width / 200.0) * height); - error_label_->setText(msg); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + error_label_->setFixedHeight((int)qCeil(width / 200.0) * height); + error_label_->setText(msg); } void LoginPage::showError(QLabel *label, const QString &msg) { - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - label->setFixedHeight((int)qCeil(width / 200.0) * height); - label->setText(msg); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + label->setFixedHeight((int)qCeil(width / 200.0) * height); + label->setText(msg); } void LoginPage::onMatrixIdEntered() { - error_label_->setText(""); + error_label_->setText(""); - User user; + User user; - if (!matrixid_input_->isValid()) { - error_matrixid_label_->show(); - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; + if (!matrixid_input_->isValid()) { + error_matrixid_label_->show(); + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } else { + error_matrixid_label_->setText(""); + error_matrixid_label_->hide(); + } + + try { + user = parse(matrixid_input_->text().toStdString()); + } catch (const std::exception &) { + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } + + QString homeServer = QString::fromStdString(user.hostname()); + if (homeServer != inferredServerAddress_) { + serverInput_->hide(); + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + if (serverInput_->isVisible()) { + matrixidLayout_->removeWidget(spinner_); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); } else { - error_matrixid_label_->setText(""); - error_matrixid_label_->hide(); + serverLayout_->removeWidget(spinner_); + matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); } - try { - user = parse(matrixid_input_->text().toStdString()); - } catch (const std::exception &) { - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; - } + inferredServerAddress_ = homeServer; + serverInput_->setText(homeServer); - QString homeServer = QString::fromStdString(user.hostname()); - if (homeServer != inferredServerAddress_) { - serverInput_->hide(); - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - if (serverInput_->isVisible()) { - matrixidLayout_->removeWidget(spinner_); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); - } else { - serverLayout_->removeWidget(spinner_); - matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); - } + http::client()->set_server(user.hostname()); + http::client()->verify_certificates( + !UserSettings::instance()->disableCertificateValidation()); - inferredServerAddress_ = homeServer; - serverInput_->setText(homeServer); + http::client()->well_known( + [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + nhlog::net()->info("Autodiscovery: No .well-known."); + checkHomeserverVersion(); + return; + } - http::client()->set_server(user.hostname()); - http::client()->verify_certificates( - !UserSettings::instance()->disableCertificateValidation()); + if (!err->parse_error.empty()) { + emit versionErrorCb(tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error("Autodiscovery failed. Received malformed response."); + return; + } - http::client()->well_known([this](const mtx::responses::WellKnown &res, - mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - nhlog::net()->info("Autodiscovery: No .well-known."); - checkHomeserverVersion(); - return; - } + emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known. {} {}", + err->status_code, + err->error_code); + return; + } - if (!err->parse_error.empty()) { - emit versionErrorCb( - tr("Autodiscovery failed. Received malformed response.")); - nhlog::net()->error( - "Autodiscovery failed. Received malformed response."); - return; - } - - emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " - "requesting .well-known.")); - nhlog::net()->error("Autodiscovery failed. Unknown error when " - "requesting .well-known. {} {}", - err->status_code, - err->error_code); - return; - } - - nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + - "'"); - http::client()->set_server(res.homeserver.base_url); - checkHomeserverVersion(); - }); - } + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); + http::client()->set_server(res.homeserver.base_url); + checkHomeserverVersion(); + }); + } } void LoginPage::checkHomeserverVersion() { - http::client()->versions( - [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - emit versionErrorCb(tr("The required endpoints were not found. " - "Possibly not a Matrix server.")); - return; - } + http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + emit versionErrorCb(tr("The required endpoints were not found. " + "Possibly not a Matrix server.")); + return; + } - if (!err->parse_error.empty()) { - emit versionErrorCb(tr("Received malformed response. Make sure " - "the homeserver domain is valid.")); - return; - } + if (!err->parse_error.empty()) { + emit versionErrorCb(tr("Received malformed response. Make sure " + "the homeserver domain is valid.")); + return; + } - emit versionErrorCb(tr( - "An unknown error occured. Make sure the homeserver domain is valid.")); - return; + emit versionErrorCb( + tr("An unknown error occured. Make sure the homeserver domain is valid.")); + return; + } + + http::client()->get_login( + [this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) { + if (err || flows.flows.empty()) + emit versionOkCb(true, false); + + bool ssoSupported_ = false; + bool passwordSupported_ = false; + for (const auto &flow : flows.flows) { + if (flow.type == mtx::user_interactive::auth_types::sso) { + ssoSupported_ = true; + } else if (flow.type == mtx::user_interactive::auth_types::password) { + passwordSupported_ = true; } - - http::client()->get_login( - [this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) { - if (err || flows.flows.empty()) - emit versionOkCb(true, false); - - bool ssoSupported_ = false; - bool passwordSupported_ = false; - for (const auto &flow : flows.flows) { - if (flow.type == mtx::user_interactive::auth_types::sso) { - ssoSupported_ = true; - } else if (flow.type == - mtx::user_interactive::auth_types::password) { - passwordSupported_ = true; - } - } - emit versionOkCb(passwordSupported_, ssoSupported_); - }); + } + emit versionOkCb(passwordSupported_, ssoSupported_); }); + }); } void LoginPage::onServerAddressEntered() { - error_label_->setText(""); - http::client()->verify_certificates( - !UserSettings::instance()->disableCertificateValidation()); - http::client()->set_server(serverInput_->text().toStdString()); - checkHomeserverVersion(); + error_label_->setText(""); + http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation()); + http::client()->set_server(serverInput_->text().toStdString()); + checkHomeserverVersion(); - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); } void LoginPage::versionError(const QString &error) { - showError(error_label_, error); - serverInput_->show(); + showError(error_label_, error); + serverInput_->show(); - spinner_->stop(); - serverLayout_->removeWidget(spinner_); - serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); - errorIcon_->show(); - matrixidLayout_->removeWidget(spinner_); + spinner_->stop(); + serverLayout_->removeWidget(spinner_); + serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); + errorIcon_->show(); + matrixidLayout_->removeWidget(spinner_); } void LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_) { - passwordSupported = passwordSupported_; - ssoSupported = ssoSupported_; + passwordSupported = passwordSupported_; + ssoSupported = ssoSupported_; - serverLayout_->removeWidget(spinner_); - matrixidLayout_->removeWidget(spinner_); - spinner_->stop(); + serverLayout_->removeWidget(spinner_); + matrixidLayout_->removeWidget(spinner_); + spinner_->stop(); - sso_login_button_->setVisible(ssoSupported); - login_button_->setVisible(passwordSupported); + sso_login_button_->setVisible(ssoSupported); + login_button_->setVisible(passwordSupported); - if (serverInput_->isVisible()) - serverInput_->hide(); + if (serverInput_->isVisible()) + serverInput_->hide(); } void LoginPage::onLoginButtonClicked(LoginMethod loginMethod) { - error_label_->setText(""); - User user; + error_label_->setText(""); + User user; - if (!matrixid_input_->isValid()) { - error_matrixid_label_->show(); - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; - } else { - error_matrixid_label_->setText(""); - error_matrixid_label_->hide(); - } + if (!matrixid_input_->isValid()) { + error_matrixid_label_->show(); + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } else { + error_matrixid_label_->setText(""); + error_matrixid_label_->hide(); + } - try { - user = parse(matrixid_input_->text().toStdString()); - } catch (const std::exception &) { - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; - } + try { + user = parse(matrixid_input_->text().toStdString()); + } catch (const std::exception &) { + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } - if (loginMethod == LoginMethod::Password) { - if (password_input_->text().isEmpty()) - return showError(error_label_, tr("Empty password")); + if (loginMethod == LoginMethod::Password) { + if (password_input_->text().isEmpty()) + return showError(error_label_, tr("Empty password")); - http::client()->login( - user.localpart(), - password_input_->text().toStdString(), - deviceName_->text().trimmed().isEmpty() ? initialDeviceName() - : deviceName_->text().toStdString(), - [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { - if (err) { - auto error = err->matrix_error.error; - if (error.empty()) - error = err->parse_error; + http::client()->login( + user.localpart(), + password_input_->text().toStdString(), + deviceName_->text().trimmed().isEmpty() ? initialDeviceName() + : deviceName_->text().toStdString(), + [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { + if (err) { + auto error = err->matrix_error.error; + if (error.empty()) + error = err->parse_error; - showErrorMessage(error_label_, QString::fromStdString(error)); - emit errorOccurred(); - return; - } + showErrorMessage(error_label_, QString::fromStdString(error)); + emit errorOccurred(); + return; + } - if (res.well_known) { - http::client()->set_server(res.well_known->homeserver.base_url); - nhlog::net()->info("Login requested to user server: " + - res.well_known->homeserver.base_url); - } + if (res.well_known) { + http::client()->set_server(res.well_known->homeserver.base_url); + nhlog::net()->info("Login requested to user server: " + + res.well_known->homeserver.base_url); + } - emit loginOk(res); - }); - } else { - auto sso = new SSOHandler(); - connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) { - mtx::requests::Login req{}; - req.token = token; - req.type = mtx::user_interactive::auth_types::token; - req.device_id = deviceName_->text().trimmed().isEmpty() - ? initialDeviceName() - : deviceName_->text().toStdString(); - http::client()->login( - req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { - if (err) { - showErrorMessage( - error_label_, - QString::fromStdString(err->matrix_error.error)); - emit errorOccurred(); - return; - } + emit loginOk(res); + }); + } else { + auto sso = new SSOHandler(); + connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) { + mtx::requests::Login req{}; + req.token = token; + req.type = mtx::user_interactive::auth_types::token; + req.device_id = deviceName_->text().trimmed().isEmpty() + ? initialDeviceName() + : deviceName_->text().toStdString(); + http::client()->login( + req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { + if (err) { + showErrorMessage(error_label_, + QString::fromStdString(err->matrix_error.error)); + emit errorOccurred(); + return; + } - if (res.well_known) { - http::client()->set_server( - res.well_known->homeserver.base_url); - nhlog::net()->info("Login requested to user server: " + - res.well_known->homeserver.base_url); - } + if (res.well_known) { + http::client()->set_server(res.well_known->homeserver.base_url); + nhlog::net()->info("Login requested to user server: " + + res.well_known->homeserver.base_url); + } - emit loginOk(res); - }); - sso->deleteLater(); - }); - connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() { - showErrorMessage(error_label_, tr("SSO login failed")); - emit errorOccurred(); - sso->deleteLater(); - }); + emit loginOk(res); + }); + sso->deleteLater(); + }); + connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() { + showErrorMessage(error_label_, tr("SSO login failed")); + emit errorOccurred(); + sso->deleteLater(); + }); - QDesktopServices::openUrl( - QString::fromStdString(http::client()->login_sso_redirect(sso->url()))); - } + QDesktopServices::openUrl( + QString::fromStdString(http::client()->login_sso_redirect(sso->url()))); + } - emit loggingIn(); + emit loggingIn(); } void LoginPage::reset() { - matrixid_input_->clear(); - password_input_->clear(); - password_input_->show(); - serverInput_->clear(); + matrixid_input_->clear(); + password_input_->clear(); + password_input_->show(); + serverInput_->clear(); - spinner_->stop(); - errorIcon_->hide(); - serverLayout_->removeWidget(spinner_); - serverLayout_->removeWidget(errorIcon_); - matrixidLayout_->removeWidget(spinner_); + spinner_->stop(); + errorIcon_->hide(); + serverLayout_->removeWidget(spinner_); + serverLayout_->removeWidget(errorIcon_); + matrixidLayout_->removeWidget(spinner_); - inferredServerAddress_.clear(); + inferredServerAddress_.clear(); } void LoginPage::onBackButtonClicked() { - emit backButtonClicked(); + emit backButtonClicked(); } void LoginPage::paintEvent(QPaintEvent *) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } diff --git a/src/LoginPage.h b/src/LoginPage.h index 2e1eb9b9..01dd27e1 100644 --- a/src/LoginPage.h +++ b/src/LoginPage.h @@ -24,101 +24,101 @@ struct Login; class LoginPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - enum class LoginMethod - { - Password, - SSO, - }; + enum class LoginMethod + { + Password, + SSO, + }; - LoginPage(QWidget *parent = nullptr); + LoginPage(QWidget *parent = nullptr); - void reset(); + void reset(); signals: - void backButtonClicked(); - void loggingIn(); - void errorOccurred(); + void backButtonClicked(); + void loggingIn(); + void errorOccurred(); - //! Used to trigger the corresponding slot outside of the main thread. - void versionErrorCb(const QString &err); - void versionOkCb(bool passwordSupported, bool ssoSupported); + //! Used to trigger the corresponding slot outside of the main thread. + void versionErrorCb(const QString &err); + void versionOkCb(bool passwordSupported, bool ssoSupported); - void loginOk(const mtx::responses::Login &res); - void showErrorMessage(QLabel *label, const QString &msg); + void loginOk(const mtx::responses::Login &res); + void showErrorMessage(QLabel *label, const QString &msg); protected: - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; public slots: - // Displays errors produced during the login. - void showError(const QString &msg); - void showError(QLabel *label, const QString &msg); + // Displays errors produced during the login. + void showError(const QString &msg); + void showError(QLabel *label, const QString &msg); private slots: - // Callback for the back button. - void onBackButtonClicked(); + // Callback for the back button. + void onBackButtonClicked(); - // Callback for the login button. - void onLoginButtonClicked(LoginMethod loginMethod); + // Callback for the login button. + void onLoginButtonClicked(LoginMethod loginMethod); - // Callback for probing the server found in the mxid - void onMatrixIdEntered(); + // Callback for probing the server found in the mxid + void onMatrixIdEntered(); - // Callback for probing the manually entered server - void onServerAddressEntered(); + // Callback for probing the manually entered server + void onServerAddressEntered(); - // Callback for errors produced during server probing - void versionError(const QString &error_message); - // Callback for successful server probing - void versionOk(bool passwordSupported, bool ssoSupported); + // Callback for errors produced during server probing + void versionError(const QString &error_message); + // Callback for successful server probing + void versionOk(bool passwordSupported, bool ssoSupported); private: - void checkHomeserverVersion(); - std::string initialDeviceName() - { + void checkHomeserverVersion(); + std::string initialDeviceName() + { #if defined(Q_OS_MAC) - return "Nheko on macOS"; + return "Nheko on macOS"; #elif defined(Q_OS_LINUX) - return "Nheko on Linux"; + return "Nheko on Linux"; #elif defined(Q_OS_WIN) - return "Nheko on Windows"; + return "Nheko on Windows"; #elif defined(Q_OS_FREEBSD) - return "Nheko on FreeBSD"; + return "Nheko on FreeBSD"; #else - return "Nheko"; + return "Nheko"; #endif - } + } - QVBoxLayout *top_layout_; + QVBoxLayout *top_layout_; - QHBoxLayout *top_bar_layout_; - QHBoxLayout *logo_layout_; - QHBoxLayout *button_layout_; + QHBoxLayout *top_bar_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; - QLabel *logo_; - QLabel *error_label_; - QLabel *error_matrixid_label_; + QLabel *logo_; + QLabel *error_label_; + QLabel *error_matrixid_label_; - QHBoxLayout *serverLayout_; - QHBoxLayout *matrixidLayout_; - LoadingIndicator *spinner_; - QLabel *errorIcon_; - QString inferredServerAddress_; + QHBoxLayout *serverLayout_; + QHBoxLayout *matrixidLayout_; + LoadingIndicator *spinner_; + QLabel *errorIcon_; + QString inferredServerAddress_; - FlatButton *back_button_; - RaisedButton *login_button_, *sso_login_button_; + FlatButton *back_button_; + RaisedButton *login_button_, *sso_login_button_; - QWidget *form_widget_; - QHBoxLayout *form_wrapper_; - QVBoxLayout *form_layout_; + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; - TextField *matrixid_input_; - TextField *password_input_; - TextField *deviceName_; - TextField *serverInput_; - bool passwordSupported = true; - bool ssoSupported = false; + TextField *matrixid_input_; + TextField *password_input_; + TextField *deviceName_; + TextField *serverInput_; + bool passwordSupported = true; + bool ssoSupported = false; }; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 8bc90f29..34db0d1d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -17,6 +16,7 @@ #include "Cache_p.h" #include "ChatPage.h" #include "Config.h" +#include "JdenticonProvider.h" #include "Logging.h" #include "LoginPage.h" #include "MainWindow.h" @@ -26,16 +26,13 @@ #include "TrayIcon.h" #include "UserSettingsPage.h" #include "Utils.h" -#include "WebRTCSession.h" #include "WelcomePage.h" #include "ui/LoadingIndicator.h" #include "ui/OverlayModal.h" #include "ui/SnackBar.h" +#include "voip/WebRTCSession.h" #include "dialogs/CreateRoom.h" -#include "dialogs/JoinRoom.h" -#include "dialogs/LeaveRoom.h" -#include "dialogs/Logout.h" MainWindow *MainWindow::instance_ = nullptr; @@ -43,438 +40,366 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , userSettings_{UserSettings::instance()} { - instance_ = this; + instance_ = this; - setWindowTitle(0); - setObjectName("MainWindow"); + setWindowTitle(0); + setObjectName("MainWindow"); - modal_ = new OverlayModal(this); + modal_ = new OverlayModal(this); - restoreWindowSize(); + restoreWindowSize(); - QFont font; - font.setStyleStrategy(QFont::PreferAntialias); - setFont(font); + QFont font; + font.setStyleStrategy(QFont::PreferAntialias); + setFont(font); - trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); + trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); - welcome_page_ = new WelcomePage(this); - login_page_ = new LoginPage(this); - register_page_ = new RegisterPage(this); - chat_page_ = new ChatPage(userSettings_, this); - userSettingsPage_ = new UserSettingsPage(userSettings_, this); + welcome_page_ = new WelcomePage(this); + login_page_ = new LoginPage(this); + register_page_ = new RegisterPage(this); + chat_page_ = new ChatPage(userSettings_, this); + userSettingsPage_ = new UserSettingsPage(userSettings_, this); - // Initialize sliding widget manager. - pageStack_ = new QStackedWidget(this); - pageStack_->addWidget(welcome_page_); - pageStack_->addWidget(login_page_); - pageStack_->addWidget(register_page_); - pageStack_->addWidget(chat_page_); - pageStack_->addWidget(userSettingsPage_); + // Initialize sliding widget manager. + pageStack_ = new QStackedWidget(this); + pageStack_->addWidget(welcome_page_); + pageStack_->addWidget(login_page_); + pageStack_->addWidget(register_page_); + pageStack_->addWidget(chat_page_); + pageStack_->addWidget(userSettingsPage_); - setCentralWidget(pageStack_); + setCentralWidget(pageStack_); - connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); - connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); + connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); + connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); - connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar); - connect( - register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar); - connect( - login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); - connect(register_page_, &RegisterPage::errorOccurred, this, [this]() { - removeOverlayProgressBar(); - }); - connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar); + connect(register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar); + connect(login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); + connect( + register_page_, &RegisterPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); + connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage); - connect( - chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar); - connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle); - connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); - connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) { - login_page_->showError(msg); - showLoginPage(); - }); + connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage); + connect( + chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar); + connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle); + connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); + connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) { + login_page_->showError(msg); + showLoginPage(); + }); - connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() { - pageStack_->setCurrentWidget(chat_page_); - }); + connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() { + pageStack_->setCurrentWidget(chat_page_); + }); - connect( - userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); - connect( - userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); - connect(trayIcon_, - SIGNAL(activated(QSystemTrayIcon::ActivationReason)), - this, - SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); + connect(userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); + connect( + userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); + connect(trayIcon_, + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + this, + SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); - connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); + connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); - connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged); + connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged); - connect( - chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); + connect(chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); - connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { - http::client()->set_user(res.user_id); - showChatPage(); - }); + connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { + http::client()->set_user(res.user_id); + showChatPage(); + }); - connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage); + connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage); - QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); - connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); + QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); + connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); - trayIcon_->setVisible(userSettings_->tray()); + trayIcon_->setVisible(userSettings_->tray()); + // load cache on event loop + QTimer::singleShot(0, this, [this] { if (hasActiveUser()) { - QString token = userSettings_->accessToken(); - QString home_server = userSettings_->homeserver(); - QString user_id = userSettings_->userId(); - QString device_id = userSettings_->deviceId(); + QString token = userSettings_->accessToken(); + QString home_server = userSettings_->homeserver(); + QString user_id = userSettings_->userId(); + QString device_id = userSettings_->deviceId(); - http::client()->set_access_token(token.toStdString()); - http::client()->set_server(home_server.toStdString()); - http::client()->set_device_id(device_id.toStdString()); + http::client()->set_access_token(token.toStdString()); + http::client()->set_server(home_server.toStdString()); + http::client()->set_device_id(device_id.toStdString()); - try { - using namespace mtx::identifiers; - http::client()->set_user(parse(user_id.toStdString())); - } catch (const std::invalid_argument &) { - nhlog::ui()->critical("bootstrapped with invalid user_id: {}", - user_id.toStdString()); - } + try { + using namespace mtx::identifiers; + http::client()->set_user(parse(user_id.toStdString())); + } catch (const std::invalid_argument &) { + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", + user_id.toStdString()); + } - showChatPage(); - } - - if (loadJdenticonPlugin()) { - nhlog::ui()->info("loaded jdenticon."); + showChatPage(); } + }); } void MainWindow::setWindowTitle(int notificationCount) { - QString name = "nheko"; + QString name = "nheko"; - if (!userSettings_.data()->profile().isEmpty()) - name += " | " + userSettings_.data()->profile(); - if (notificationCount > 0) { - name.append(QString{" (%1)"}.arg(notificationCount)); - } - QMainWindow::setWindowTitle(name); + if (!userSettings_.data()->profile().isEmpty()) + name += " | " + userSettings_.data()->profile(); + if (notificationCount > 0) { + name.append(QString{" (%1)"}.arg(notificationCount)); + } + QMainWindow::setWindowTitle(name); } bool MainWindow::event(QEvent *event) { - auto type = event->type(); - if (type == QEvent::WindowActivate) { - emit focusChanged(true); - } else if (type == QEvent::WindowDeactivate) { - emit focusChanged(false); - } + auto type = event->type(); + if (type == QEvent::WindowActivate) { + emit focusChanged(true); + } else if (type == QEvent::WindowDeactivate) { + emit focusChanged(false); + } - return QMainWindow::event(event); + return QMainWindow::event(event); } void MainWindow::restoreWindowSize() { - QSettings settings; - int savedWidth = settings.value("window/width").toInt(); - int savedheight = settings.value("window/height").toInt(); + int savedWidth = userSettings_->qsettings()->value("window/width").toInt(); + int savedheight = userSettings_->qsettings()->value("window/height").toInt(); - if (savedWidth == 0 || savedheight == 0) - resize(conf::window::width, conf::window::height); - else - resize(savedWidth, savedheight); + nhlog::ui()->info("Restoring window size {}x{}", savedWidth, savedheight); + + if (savedWidth == 0 || savedheight == 0) + resize(conf::window::width, conf::window::height); + else + resize(savedWidth, savedheight); } void MainWindow::saveCurrentWindowSize() { - QSettings settings; - QSize current = size(); + auto settings = userSettings_->qsettings(); + QSize current = size(); - settings.setValue("window/width", current.width()); - settings.setValue("window/height", current.height()); + settings->setValue("window/width", current.width()); + settings->setValue("window/height", current.height()); } void MainWindow::removeOverlayProgressBar() { - QTimer *timer = new QTimer(this); - timer->setSingleShot(true); + QTimer *timer = new QTimer(this); + timer->setSingleShot(true); - connect(timer, &QTimer::timeout, [this, timer]() { - timer->deleteLater(); + connect(timer, &QTimer::timeout, [this, timer]() { + timer->deleteLater(); - if (modal_) - modal_->hide(); + if (modal_) + modal_->hide(); - if (spinner_) - spinner_->stop(); - }); + if (spinner_) + spinner_->stop(); + }); - // FIXME: Snackbar doesn't work if it's initialized in the constructor. - QTimer::singleShot(0, this, [this]() { - snackBar_ = new SnackBar(this); - connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage); - }); + // FIXME: Snackbar doesn't work if it's initialized in the constructor. + QTimer::singleShot(0, this, [this]() { + snackBar_ = new SnackBar(this); + connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage); + }); - timer->start(50); + timer->start(50); } void MainWindow::showChatPage() { - auto userid = QString::fromStdString(http::client()->user_id().to_string()); - auto device_id = QString::fromStdString(http::client()->device_id()); - auto homeserver = QString::fromStdString(http::client()->server() + ":" + - std::to_string(http::client()->port())); - auto token = QString::fromStdString(http::client()->access_token()); + auto userid = QString::fromStdString(http::client()->user_id().to_string()); + auto device_id = QString::fromStdString(http::client()->device_id()); + auto homeserver = QString::fromStdString(http::client()->server() + ":" + + std::to_string(http::client()->port())); + auto token = QString::fromStdString(http::client()->access_token()); - userSettings_.data()->setUserId(userid); - userSettings_.data()->setAccessToken(token); - userSettings_.data()->setDeviceId(device_id); - userSettings_.data()->setHomeserver(homeserver); + userSettings_.data()->setUserId(userid); + userSettings_.data()->setAccessToken(token); + userSettings_.data()->setDeviceId(device_id); + userSettings_.data()->setHomeserver(homeserver); - showOverlayProgressBar(); + showOverlayProgressBar(); - pageStack_->setCurrentWidget(chat_page_); + pageStack_->setCurrentWidget(chat_page_); - pageStack_->removeWidget(welcome_page_); - pageStack_->removeWidget(login_page_); - pageStack_->removeWidget(register_page_); + pageStack_->removeWidget(welcome_page_); + pageStack_->removeWidget(login_page_); + pageStack_->removeWidget(register_page_); - login_page_->reset(); - chat_page_->bootstrap(userid, homeserver, token); - connect(cache::client(), - &Cache::secretChanged, - userSettingsPage_, - &UserSettingsPage::updateSecretStatus); - emit reload(); + login_page_->reset(); + chat_page_->bootstrap(userid, homeserver, token); + connect(cache::client(), + &Cache::secretChanged, + userSettingsPage_, + &UserSettingsPage::updateSecretStatus); + emit reload(); } void MainWindow::closeEvent(QCloseEvent *event) { - if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { - if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") != - QMessageBox::Yes) { - event->ignore(); - return; - } + if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { + if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") != + QMessageBox::Yes) { + event->ignore(); + return; } + } - if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && - userSettings_->tray()) { - event->ignore(); - hide(); - } + if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && userSettings_->tray()) { + event->ignore(); + hide(); + } } void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) { - switch (reason) { - case QSystemTrayIcon::Trigger: - if (!isVisible()) { - show(); - } else { - hide(); - } - break; - default: - break; + switch (reason) { + case QSystemTrayIcon::Trigger: + if (!isVisible()) { + show(); + } else { + hide(); } + break; + default: + break; + } } bool MainWindow::hasActiveUser() { - QSettings settings; - QString prefix; - if (userSettings_->profile() != "") - prefix = "profile/" + userSettings_->profile() + "/"; + auto settings = userSettings_->qsettings(); + QString prefix; + if (userSettings_->profile() != "") + prefix = "profile/" + userSettings_->profile() + "/"; - return settings.contains(prefix + "auth/access_token") && - settings.contains(prefix + "auth/home_server") && - settings.contains(prefix + "auth/user_id"); -} - -void -MainWindow::openLeaveRoomDialog(const QString &room_id) -{ - auto dialog = new dialogs::LeaveRoom(this); - connect(dialog, &dialogs::LeaveRoom::leaving, this, [this, room_id]() { - chat_page_->leaveRoom(room_id); - }); - - showDialog(dialog); + return settings->contains(prefix + "auth/access_token") && + settings->contains(prefix + "auth/home_server") && + settings->contains(prefix + "auth/user_id"); } void MainWindow::showOverlayProgressBar() { - spinner_ = new LoadingIndicator(this); - spinner_->setFixedHeight(100); - spinner_->setFixedWidth(100); - spinner_->setObjectName("ChatPageLoadSpinner"); - spinner_->start(); + spinner_ = new LoadingIndicator(this); + spinner_->setFixedHeight(100); + spinner_->setFixedWidth(100); + spinner_->setObjectName("ChatPageLoadSpinner"); + spinner_->start(); - showSolidOverlayModal(spinner_); -} - -void -MainWindow::openJoinRoomDialog(std::function callback) -{ - auto dialog = new dialogs::JoinRoom(this); - connect(dialog, &dialogs::JoinRoom::joinRoom, this, [callback](const QString &room) { - if (!room.isEmpty()) - callback(room); - }); - - showDialog(dialog); + showSolidOverlayModal(spinner_); } void MainWindow::openCreateRoomDialog( std::function callback) { - auto dialog = new dialogs::CreateRoom(this); - connect(dialog, - &dialogs::CreateRoom::createRoom, - this, - [callback](const mtx::requests::CreateRoom &request) { callback(request); }); + auto dialog = new dialogs::CreateRoom(this); + connect(dialog, + &dialogs::CreateRoom::createRoom, + this, + [callback](const mtx::requests::CreateRoom &request) { callback(request); }); - showDialog(dialog); + showDialog(dialog); } void MainWindow::showTransparentOverlayModal(QWidget *content, QFlags flags) { - modal_->setWidget(content); - modal_->setColor(QColor(30, 30, 30, 150)); - modal_->setDismissible(true); - modal_->setContentAlignment(flags); - modal_->raise(); - modal_->show(); + modal_->setWidget(content); + modal_->setColor(QColor(30, 30, 30, 150)); + modal_->setDismissible(true); + modal_->setContentAlignment(flags); + modal_->raise(); + modal_->show(); } void MainWindow::showSolidOverlayModal(QWidget *content, QFlags flags) { - modal_->setWidget(content); - modal_->setColor(QColor(30, 30, 30)); - modal_->setDismissible(false); - modal_->setContentAlignment(flags); - modal_->raise(); - modal_->show(); -} - -void -MainWindow::openLogoutDialog() -{ - auto dialog = new dialogs::Logout(this); - connect(dialog, &dialogs::Logout::loggingOut, this, [this]() { - if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { - if (QMessageBox::question( - this, "nheko", "A call is in progress. Log out?") != - QMessageBox::Yes) { - return; - } - WebRTCSession::instance().end(); - } - chat_page_->initiateLogout(); - }); - - showDialog(dialog); + modal_->setWidget(content); + modal_->setColor(QColor(30, 30, 30)); + modal_->setDismissible(false); + modal_->setContentAlignment(flags); + modal_->raise(); + modal_->show(); } bool MainWindow::hasActiveDialogs() const { - return !modal_ && modal_->isVisible(); + return !modal_ && modal_->isVisible(); } bool MainWindow::pageSupportsTray() const { - return !welcome_page_->isVisible() && !login_page_->isVisible() && - !register_page_->isVisible(); + return !welcome_page_->isVisible() && !login_page_->isVisible() && !register_page_->isVisible(); } void MainWindow::hideOverlay() { - if (modal_) - modal_->hide(); + if (modal_) + modal_->hide(); } inline void MainWindow::showDialog(QWidget *dialog) { - utils::centerWidget(dialog, this); - dialog->raise(); - dialog->show(); + utils::centerWidget(dialog, this); + dialog->raise(); + dialog->show(); } -bool -MainWindow::loadJdenticonPlugin() -{ - QDir pluginsDir(qApp->applicationDirPath()); - - bool plugins = pluginsDir.cd("plugins"); - if (plugins) { - foreach (QString fileName, pluginsDir.entryList(QDir::Files)) { - QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName)); - QObject *plugin = pluginLoader.instance(); - if (plugin) { - jdenticonInteface_ = qobject_cast(plugin); - if (jdenticonInteface_) { - nhlog::ui()->info("Found jdenticon plugin."); - return true; - } - } - } - } - - nhlog::ui()->info("jdenticon plugin not found."); - return false; -} void MainWindow::showWelcomePage() { - removeOverlayProgressBar(); - pageStack_->addWidget(welcome_page_); - pageStack_->setCurrentWidget(welcome_page_); + removeOverlayProgressBar(); + pageStack_->addWidget(welcome_page_); + pageStack_->setCurrentWidget(welcome_page_); } void MainWindow::showLoginPage() { - if (modal_) - modal_->hide(); + if (modal_) + modal_->hide(); - pageStack_->addWidget(login_page_); - pageStack_->setCurrentWidget(login_page_); + pageStack_->addWidget(login_page_); + pageStack_->setCurrentWidget(login_page_); } void MainWindow::showRegisterPage() { - pageStack_->addWidget(register_page_); - pageStack_->setCurrentWidget(register_page_); + pageStack_->addWidget(register_page_); + pageStack_->setCurrentWidget(register_page_); } void MainWindow::showUserSettingsPage() { - pageStack_->setCurrentWidget(userSettingsPage_); + pageStack_->setCurrentWidget(userSettingsPage_); } diff --git a/src/MainWindow.h b/src/MainWindow.h index d423af9f..b9d2fe5f 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -37,8 +37,6 @@ struct CreateRoom; namespace dialogs { class CreateRoom; class InviteUsers; -class JoinRoom; -class LeaveRoom; class Logout; class MemberList; class ReCaptcha; @@ -46,97 +44,90 @@ class ReCaptcha; class MainWindow : public QMainWindow { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(int x READ x CONSTANT) - Q_PROPERTY(int y READ y CONSTANT) - Q_PROPERTY(int width READ width CONSTANT) - Q_PROPERTY(int height READ height CONSTANT) + Q_PROPERTY(int x READ x CONSTANT) + Q_PROPERTY(int y READ y CONSTANT) + Q_PROPERTY(int width READ width CONSTANT) + Q_PROPERTY(int height READ height CONSTANT) public: - explicit MainWindow(QWidget *parent = nullptr); + explicit MainWindow(QWidget *parent = nullptr); - static MainWindow *instance() { return instance_; } - void saveCurrentWindowSize(); + static MainWindow *instance() { return instance_; } + void saveCurrentWindowSize(); - void openLeaveRoomDialog(const QString &room_id); - void openInviteUsersDialog(std::function callback); - void openCreateRoomDialog( - std::function callback); - void openJoinRoomDialog(std::function callback); - void openLogoutDialog(); + void openCreateRoomDialog( + std::function callback); + void openJoinRoomDialog(std::function callback); + void openLogoutDialog(); - void hideOverlay(); - void showSolidOverlayModal(QWidget *content, - QFlags flags = Qt::AlignCenter); - void showTransparentOverlayModal(QWidget *content, - QFlags flags = Qt::AlignTop | - Qt::AlignHCenter); + void hideOverlay(); + void showSolidOverlayModal(QWidget *content, QFlags flags = Qt::AlignCenter); + void showTransparentOverlayModal(QWidget *content, + QFlags flags = Qt::AlignTop | + Qt::AlignHCenter); protected: - void closeEvent(QCloseEvent *event) override; - bool event(QEvent *event) override; + void closeEvent(QCloseEvent *event) override; + bool event(QEvent *event) override; private slots: - //! Handle interaction with the tray icon. - void iconActivated(QSystemTrayIcon::ActivationReason reason); + //! Handle interaction with the tray icon. + void iconActivated(QSystemTrayIcon::ActivationReason reason); - //! Show the welcome page in the main window. - void showWelcomePage(); + //! Show the welcome page in the main window. + void showWelcomePage(); - //! Show the login page in the main window. - void showLoginPage(); + //! Show the login page in the main window. + void showLoginPage(); - //! Show the register page in the main window. - void showRegisterPage(); + //! Show the register page in the main window. + void showRegisterPage(); - //! Show user settings page. - void showUserSettingsPage(); + //! Show user settings page. + void showUserSettingsPage(); - //! Show the chat page and start communicating with the given access token. - void showChatPage(); + //! Show the chat page and start communicating with the given access token. + void showChatPage(); - void showOverlayProgressBar(); - void removeOverlayProgressBar(); + void showOverlayProgressBar(); + void removeOverlayProgressBar(); - virtual void setWindowTitle(int notificationCount); + virtual void setWindowTitle(int notificationCount); signals: - void focusChanged(const bool focused); - void reload(); + void focusChanged(const bool focused); + void reload(); private: - bool loadJdenticonPlugin(); + void showDialog(QWidget *dialog); + bool hasActiveUser(); + void restoreWindowSize(); + //! Check if there is an open dialog. + bool hasActiveDialogs() const; + //! Check if the current page supports the "minimize to tray" functionality. + bool pageSupportsTray() const; - void showDialog(QWidget *dialog); - bool hasActiveUser(); - void restoreWindowSize(); - //! Check if there is an open dialog. - bool hasActiveDialogs() const; - //! Check if the current page supports the "minimize to tray" functionality. - bool pageSupportsTray() const; + static MainWindow *instance_; - static MainWindow *instance_; - - //! The initial welcome screen. - WelcomePage *welcome_page_; - //! The login screen. - LoginPage *login_page_; - //! The register page. - RegisterPage *register_page_; - //! A stacked widget that handles the transitions between widgets. - QStackedWidget *pageStack_; - //! The main chat area. - ChatPage *chat_page_; - UserSettingsPage *userSettingsPage_; - QSharedPointer userSettings_; - //! Tray icon that shows the unread message count. - TrayIcon *trayIcon_; - //! Notifications display. - SnackBar *snackBar_ = nullptr; - //! Overlay modal used to project other widgets. - OverlayModal *modal_ = nullptr; - LoadingIndicator *spinner_ = nullptr; - - JdenticonInterface *jdenticonInteface_ = nullptr; + //! The initial welcome screen. + WelcomePage *welcome_page_; + //! The login screen. + LoginPage *login_page_; + //! The register page. + RegisterPage *register_page_; + //! A stacked widget that handles the transitions between widgets. + QStackedWidget *pageStack_; + //! The main chat area. + ChatPage *chat_page_; + UserSettingsPage *userSettingsPage_; + QSharedPointer userSettings_; + //! Tray icon that shows the unread message count. + TrayIcon *trayIcon_; + //! Notifications display. + SnackBar *snackBar_ = nullptr; + //! Overlay modal used to project other widgets. + OverlayModal *modal_ = nullptr; + LoadingIndicator *spinner_ = nullptr; }; diff --git a/src/MatrixClient.cpp b/src/MatrixClient.cpp index 196a9322..2ceb53a8 100644 --- a/src/MatrixClient.cpp +++ b/src/MatrixClient.cpp @@ -37,31 +37,31 @@ namespace http { mtx::http::Client * client() { - return client_.get(); + return client_.get(); } bool is_logged_in() { - return !client_->access_token().empty(); + return !client_->access_token().empty(); } void init() { - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType>("std::map"); - qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType>(); + qRegisterMetaType>("std::map"); + qRegisterMetaType>(); } } // namespace http diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 0c0f0cdd..34730e9a 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -15,98 +15,96 @@ MemberList::MemberList(const QString &room_id, QObject *parent) : QAbstractListModel{parent} , room_id_{room_id} { - try { - info_ = cache::singleRoomInfo(room_id_.toStdString()); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve room info from cache: {}", - room_id_.toStdString()); - } + try { + info_ = cache::singleRoomInfo(room_id_.toStdString()); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); + } - try { - auto members = cache::getMembers(room_id_.toStdString()); - addUsers(members); - numUsersLoaded_ = members.size(); - } catch (const lmdb::error &e) { - nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); - } + try { + auto members = cache::getMembers(room_id_.toStdString()); + addUsers(members); + numUsersLoaded_ = members.size(); + } catch (const lmdb::error &e) { + nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); + } } void MemberList::addUsers(const std::vector &members) { - beginInsertRows( - QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); + beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); - for (const auto &member : members) - m_memberList.push_back( - {member, - ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl( - member.user_id)}); + for (const auto &member : members) + m_memberList.push_back( + {member, + ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl( + member.user_id)}); - endInsertRows(); + endInsertRows(); } QHash MemberList::roleNames() const { - return { - {Mxid, "mxid"}, - {DisplayName, "displayName"}, - {AvatarUrl, "avatarUrl"}, - {Trustlevel, "trustlevel"}, - }; + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Trustlevel, "trustlevel"}, + }; } QVariant MemberList::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0) - return {}; + if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0) + return {}; - switch (role) { - case Mxid: - return m_memberList[index.row()].first.user_id; - case DisplayName: - return m_memberList[index.row()].first.display_name; - case AvatarUrl: - return m_memberList[index.row()].second; - case Trustlevel: { - auto stat = - cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString()); + switch (role) { + case Mxid: + return m_memberList[index.row()].first.user_id; + case DisplayName: + return m_memberList[index.row()].first.display_name; + case AvatarUrl: + return m_memberList[index.row()].second; + case Trustlevel: { + auto stat = + cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString()); - if (!stat) - return crypto::Unverified; - if (stat->unverified_device_count) - return crypto::Unverified; - else - return stat->user_verified; - } - default: - return {}; - } + if (!stat) + return crypto::Unverified; + if (stat->unverified_device_count) + return crypto::Unverified; + else + return stat->user_verified; + } + default: + return {}; + } } bool MemberList::canFetchMore(const QModelIndex &) const { - const size_t numMembers = rowCount(); - if (numMembers > 1 && numMembers < info_.member_count) - return true; - else - return false; + const size_t numMembers = rowCount(); + if (numMembers > 1 && numMembers < info_.member_count) + return true; + else + return false; } void MemberList::fetchMore(const QModelIndex &) { - loadingMoreMembers_ = true; - emit loadingMoreMembersChanged(); + loadingMoreMembers_ = true; + emit loadingMoreMembersChanged(); - auto members = cache::getMembers(room_id_.toStdString(), rowCount()); - addUsers(members); - numUsersLoaded_ += members.size(); - emit numUsersLoadedChanged(); + auto members = cache::getMembers(room_id_.toStdString(), rowCount()); + addUsers(members); + numUsersLoaded_ += members.size(); + emit numUsersLoadedChanged(); - loadingMoreMembers_ = false; - emit loadingMoreMembersChanged(); + loadingMoreMembers_ = false; + emit loadingMoreMembersChanged(); } diff --git a/src/MemberList.h b/src/MemberList.h index cffcd83d..b16ac983 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -10,59 +10,59 @@ class MemberList : public QAbstractListModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) - Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) - Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) - Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) - Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged) + Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) + Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) + Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged) public: - enum Roles - { - Mxid, - DisplayName, - AvatarUrl, - Trustlevel, - }; - MemberList(const QString &room_id, QObject *parent = nullptr); + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + Trustlevel, + }; + MemberList(const QString &room_id, QObject *parent = nullptr); - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - Q_UNUSED(parent) - return static_cast(m_memberList.size()); - } - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + Q_UNUSED(parent) + return static_cast(m_memberList.size()); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QString roomName() const { return QString::fromStdString(info_.name); } - int memberCount() const { return info_.member_count; } - QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } - QString roomId() const { return room_id_; } - int numUsersLoaded() const { return numUsersLoaded_; } - bool loadingMoreMembers() const { return loadingMoreMembers_; } + QString roomName() const { return QString::fromStdString(info_.name); } + int memberCount() const { return info_.member_count; } + QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } + QString roomId() const { return room_id_; } + int numUsersLoaded() const { return numUsersLoaded_; } + bool loadingMoreMembers() const { return loadingMoreMembers_; } signals: - void roomNameChanged(); - void memberCountChanged(); - void avatarUrlChanged(); - void roomIdChanged(); - void numUsersLoadedChanged(); - void loadingMoreMembersChanged(); + void roomNameChanged(); + void memberCountChanged(); + void avatarUrlChanged(); + void roomIdChanged(); + void numUsersLoadedChanged(); + void loadingMoreMembersChanged(); public slots: - void addUsers(const std::vector &users); + void addUsers(const std::vector &users); protected: - bool canFetchMore(const QModelIndex &) const override; - void fetchMore(const QModelIndex &) override; + bool canFetchMore(const QModelIndex &) const override; + void fetchMore(const QModelIndex &) override; private: - QVector> m_memberList; - QString room_id_; - RoomInfo info_; - int numUsersLoaded_{0}; - bool loadingMoreMembers_{false}; + QVector> m_memberList; + QString room_id_; + RoomInfo info_; + int numUsersLoaded_{0}; + bool loadingMoreMembers_{false}; }; diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 056374a9..5d0ee0be 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -24,70 +24,70 @@ QHash infos; QQuickImageResponse * MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) { - auto id_ = id; - bool crop = true; - double radius = 0; + auto id_ = id; + bool crop = true; + double radius = 0; - auto queryStart = id.lastIndexOf('?'); - if (queryStart != -1) { - id_ = id.left(queryStart); - auto query = id.midRef(queryStart + 1); - auto queryBits = query.split('&'); + auto queryStart = id.lastIndexOf('?'); + if (queryStart != -1) { + id_ = id.left(queryStart); + auto query = id.midRef(queryStart + 1); + auto queryBits = query.split('&'); - for (auto b : queryBits) { - if (b == "scale") { - crop = false; - } else if (b.startsWith("radius=")) { - radius = b.mid(7).toDouble(); - } - } + for (auto b : queryBits) { + if (b == "scale") { + crop = false; + } else if (b.startsWith("radius=")) { + radius = b.mid(7).toDouble(); + } } + } - MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize); - pool.start(response); - return response; + MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize); + pool.start(response); + return response; } void MxcImageProvider::addEncryptionInfo(mtx::crypto::EncryptedFile info) { - infos.insert(QString::fromStdString(info.url), info); + infos.insert(QString::fromStdString(info.url), info); } void MxcImageResponse::run() { - MxcImageProvider::download( - m_id, - m_requestedSize, - [this](QString, QSize, QImage image, QString) { - if (image.isNull()) { - m_error = "Failed to download image."; - } else { - m_image = image; - } - emit finished(); - }, - m_crop, - m_radius); + MxcImageProvider::download( + m_id, + m_requestedSize, + [this](QString, QSize, QImage image, QString) { + if (image.isNull()) { + m_error = "Failed to download image."; + } else { + m_image = image; + } + emit finished(); + }, + m_crop, + m_radius); } static QImage clipRadius(QImage img, double radius) { - QImage out(img.size(), QImage::Format_ARGB32_Premultiplied); - out.fill(Qt::transparent); + QImage out(img.size(), QImage::Format_ARGB32_Premultiplied); + out.fill(Qt::transparent); - QPainter painter(&out); - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + QPainter painter(&out); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - QPainterPath ppath; - ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); + QPainterPath ppath; + ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); - painter.setClipPath(ppath); - painter.drawImage(img.rect(), img); + painter.setClipPath(ppath); + painter.drawImage(img.rect(), img); - return out; + return out; } void @@ -97,187 +97,165 @@ MxcImageProvider::download(const QString &id, bool crop, double radius) { - std::optional encryptionInfo; - auto temp = infos.find("mxc://" + id); - if (temp != infos.end()) - encryptionInfo = *temp; + std::optional encryptionInfo; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + encryptionInfo = *temp; - if (requestedSize.isValid() && !encryptionInfo) { - QString fileName = - QString("%1_%2x%3_%4_radius%5") - .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding | - QByteArray::OmitTrailingEquals))) - .arg(requestedSize.width()) - .arg(requestedSize.height()) - .arg(crop ? "crop" : "scale") - .arg(radius); - QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + - "/media_cache", - fileName); - QDir().mkpath(fileInfo.absolutePath()); + if (requestedSize.isValid() && !encryptionInfo) { + QString fileName = QString("%1_%2x%3_%4_radius%5") + .arg(QString::fromUtf8(id.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) + .arg(requestedSize.width()) + .arg(requestedSize.height()) + .arg(crop ? "crop" : "scale") + .arg(radius); + QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/media_cache", + fileName); + QDir().mkpath(fileInfo.absolutePath()); - if (fileInfo.exists()) { - QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); - if (!image.isNull()) { - image = image.scaled( - requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + if (fileInfo.exists()) { + QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); + if (!image.isNull()) { + image = image.scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - - if (!image.isNull()) { - then(id, requestedSize, image, fileInfo.absoluteFilePath()); - return; - } - } + if (radius != 0) { + image = clipRadius(std::move(image), radius); } - mtx::http::ThumbOpts opts; - opts.mxc_url = "mxc://" + id.toStdString(); - opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; - opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; - opts.method = crop ? "crop" : "scale"; - http::client()->get_thumbnail( - opts, - [fileInfo, requestedSize, radius, then, id](const std::string &res, - mtx::http::RequestErr err) { - if (err || res.empty()) { - then(id, QSize(), {}, ""); - - return; - } - - auto data = QByteArray(res.data(), (int)res.size()); - QImage image = utils::readImage(data); - if (!image.isNull()) { - image = image.scaled( - requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - } - image.setText("mxc url", "mxc://" + id); - if (image.save(fileInfo.absoluteFilePath(), "png")) - nhlog::ui()->debug("Wrote: {}", - fileInfo.absoluteFilePath().toStdString()); - else - nhlog::ui()->debug("Failed to write: {}", - fileInfo.absoluteFilePath().toStdString()); - - then(id, requestedSize, image, fileInfo.absoluteFilePath()); - }); - } else { - try { - QString fileName = - QString("%1_radius%2") - .arg(QString::fromUtf8(id.toUtf8().toBase64( - QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) - .arg(radius); - - QFileInfo fileInfo( - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + - "/media_cache", - fileName); - QDir().mkpath(fileInfo.absolutePath()); - - if (fileInfo.exists()) { - if (encryptionInfo) { - QFile f(fileInfo.absoluteFilePath()); - f.open(QIODevice::ReadOnly); - - QByteArray fileData = f.readAll(); - auto tempData = - mtx::crypto::to_string(mtx::crypto::decrypt_file( - fileData.toStdString(), encryptionInfo.value())); - auto data = - QByteArray(tempData.data(), (int)tempData.size()); - QImage image = utils::readImage(data); - image.setText("mxc url", "mxc://" + id); - if (!image.isNull()) { - if (radius != 0) { - image = - clipRadius(std::move(image), radius); - } - - then(id, - requestedSize, - image, - fileInfo.absoluteFilePath()); - return; - } - } else { - QImage image = - utils::readImageFromFile(fileInfo.absoluteFilePath()); - if (!image.isNull()) { - if (radius != 0) { - image = - clipRadius(std::move(image), radius); - } - - then(id, - requestedSize, - image, - fileInfo.absoluteFilePath()); - return; - } - } - } - - http::client()->download( - "mxc://" + id.toStdString(), - [fileInfo, requestedSize, then, id, radius, encryptionInfo]( - const std::string &res, - const std::string &, - const std::string &originalFilename, - mtx::http::RequestErr err) { - if (err) { - then(id, QSize(), {}, ""); - return; - } - - auto tempData = res; - QFile f(fileInfo.absoluteFilePath()); - if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) { - then(id, QSize(), {}, ""); - return; - } - f.write(tempData.data(), tempData.size()); - f.close(); - - if (encryptionInfo) { - tempData = - mtx::crypto::to_string(mtx::crypto::decrypt_file( - tempData, encryptionInfo.value())); - auto data = - QByteArray(tempData.data(), (int)tempData.size()); - QImage image = utils::readImage(data); - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - - image.setText("original filename", - QString::fromStdString(originalFilename)); - image.setText("mxc url", "mxc://" + id); - then( - id, requestedSize, image, fileInfo.absoluteFilePath()); - return; - } - - QImage image = - utils::readImageFromFile(fileInfo.absoluteFilePath()); - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - - image.setText("original filename", - QString::fromStdString(originalFilename)); - image.setText("mxc url", "mxc://" + id); - then(id, requestedSize, image, fileInfo.absoluteFilePath()); - }); - } catch (std::exception &e) { - nhlog::net()->error("Exception while downloading media: {}", e.what()); + if (!image.isNull()) { + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; } + } } + + mtx::http::ThumbOpts opts; + opts.mxc_url = "mxc://" + id.toStdString(); + opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; + opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; + opts.method = crop ? "crop" : "scale"; + http::client()->get_thumbnail( + opts, + [fileInfo, requestedSize, radius, then, id](const std::string &res, + mtx::http::RequestErr err) { + if (err || res.empty()) { + then(id, QSize(), {}, ""); + + return; + } + + auto data = QByteArray(res.data(), (int)res.size()); + QImage image = utils::readImage(data); + if (!image.isNull()) { + image = + image.scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + } + image.setText("mxc url", "mxc://" + id); + if (image.save(fileInfo.absoluteFilePath(), "png")) + nhlog::ui()->debug("Wrote: {}", fileInfo.absoluteFilePath().toStdString()); + else + nhlog::ui()->debug("Failed to write: {}", + fileInfo.absoluteFilePath().toStdString()); + + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + }); + } else { + try { + QString fileName = QString("%1_radius%2") + .arg(QString::fromUtf8(id.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) + .arg(radius); + + QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/media_cache", + fileName); + QDir().mkpath(fileInfo.absolutePath()); + + if (fileInfo.exists()) { + if (encryptionInfo) { + QFile f(fileInfo.absoluteFilePath()); + f.open(QIODevice::ReadOnly); + + QByteArray fileData = f.readAll(); + auto tempData = mtx::crypto::to_string( + mtx::crypto::decrypt_file(fileData.toStdString(), encryptionInfo.value())); + auto data = QByteArray(tempData.data(), (int)tempData.size()); + QImage image = utils::readImage(data); + image.setText("mxc url", "mxc://" + id); + if (!image.isNull()) { + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } + } else { + QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); + if (!image.isNull()) { + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } + } + } + + http::client()->download( + "mxc://" + id.toStdString(), + [fileInfo, requestedSize, then, id, radius, encryptionInfo]( + const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + then(id, QSize(), {}, ""); + return; + } + + auto tempData = res; + QFile f(fileInfo.absoluteFilePath()); + if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) { + then(id, QSize(), {}, ""); + return; + } + f.write(tempData.data(), tempData.size()); + f.close(); + + if (encryptionInfo) { + tempData = mtx::crypto::to_string( + mtx::crypto::decrypt_file(tempData, encryptionInfo.value())); + auto data = QByteArray(tempData.data(), (int)tempData.size()); + QImage image = utils::readImage(data); + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + + image.setText("original filename", QString::fromStdString(originalFilename)); + image.setText("mxc url", "mxc://" + id); + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } + + QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + + image.setText("original filename", QString::fromStdString(originalFilename)); + image.setText("mxc url", "mxc://" + id); + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + }); + } catch (std::exception &e) { + nhlog::net()->error("Exception while downloading media: {}", e.what()); + } + } } diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 6de83c0e..3cf5bbf4 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -19,46 +19,46 @@ class MxcImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize) - : m_id(id) - , m_requestedSize(requestedSize) - , m_crop(crop) - , m_radius(radius) - { - setAutoDelete(false); - } + MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize) + : m_id(id) + , m_requestedSize(requestedSize) + , m_crop(crop) + , m_radius(radius) + { + 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; - bool m_crop; - double m_radius; + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; + bool m_crop; + double m_radius; }; class MxcImageProvider : public QObject , public QQuickAsyncImageProvider { - Q_OBJECT + Q_OBJECT public slots: - QQuickImageResponse *requestImageResponse(const QString &id, - const QSize &requestedSize) override; + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override; - static void addEncryptionInfo(mtx::crypto::EncryptedFile info); - static void download(const QString &id, - const QSize &requestedSize, - std::function then, - bool crop = true, - double radius = 0); + static void addEncryptionInfo(mtx::crypto::EncryptedFile info); + static void download(const QString &id, + const QSize &requestedSize, + std::function then, + bool crop = true, + double radius = 0); private: - QThreadPool pool; + QThreadPool pool; }; diff --git a/src/Olm.cpp b/src/Olm.cpp deleted file mode 100644 index 2c9ac5a3..00000000 --- a/src/Olm.cpp +++ /dev/null @@ -1,1576 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "Olm.h" - -#include -#include - -#include -#include - -#include -#include - -#include "Cache.h" -#include "Cache_p.h" -#include "ChatPage.h" -#include "DeviceVerificationFlow.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "UserSettingsPage.h" -#include "Utils.h" - -namespace { -auto client_ = std::make_unique(); - -std::map request_id_to_secret_name; - -const std::string STORAGE_SECRET_KEY("secret"); -constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2"; -} - -namespace olm { -void -from_json(const nlohmann::json &obj, OlmMessage &msg) -{ - if (obj.at("type") != "m.room.encrypted") - throw std::invalid_argument("invalid type for olm message"); - - if (obj.at("content").at("algorithm") != OLM_ALGO) - throw std::invalid_argument("invalid algorithm for olm message"); - - msg.sender = obj.at("sender"); - msg.sender_key = obj.at("content").at("sender_key"); - msg.ciphertext = obj.at("content") - .at("ciphertext") - .get>(); -} - -mtx::crypto::OlmClient * -client() -{ - return client_.get(); -} - -static void -handle_secret_request(const mtx::events::DeviceEvent *e, - const std::string &sender) -{ - using namespace mtx::events; - - if (e->content.action != mtx::events::msg::RequestAction::Request) - return; - - auto local_user = http::client()->user_id(); - - if (sender != local_user.to_string()) - return; - - auto verificationStatus = cache::verificationStatus(local_user.to_string()); - - if (!verificationStatus) - return; - - auto deviceKeys = cache::userKeys(local_user.to_string()); - if (!deviceKeys) - return; - - if (std::find(verificationStatus->verified_devices.begin(), - verificationStatus->verified_devices.end(), - e->content.requesting_device_id) == - verificationStatus->verified_devices.end()) - return; - - // this is a verified device - mtx::events::DeviceEvent secretSend; - secretSend.type = EventType::SecretSend; - secretSend.content.request_id = e->content.request_id; - - auto secret = cache::client()->secret(e->content.name); - if (!secret) - return; - secretSend.content.secret = secret.value(); - - send_encrypted_to_device_messages( - {{local_user.to_string(), {{e->content.requesting_device_id}}}}, secretSend); - - nhlog::net()->info("Sent secret '{}' to ({},{})", - e->content.name, - local_user.to_string(), - e->content.requesting_device_id); -} - -void -handle_to_device_messages(const std::vector &msgs) -{ - if (msgs.empty()) - return; - nhlog::crypto()->info("received {} to_device messages", msgs.size()); - nlohmann::json j_msg; - - for (const auto &msg : msgs) { - j_msg = std::visit([](auto &e) { return json(e); }, std::move(msg)); - if (j_msg.count("type") == 0) { - nhlog::crypto()->warn("received message with no type field: {}", - j_msg.dump(2)); - continue; - } - - std::string msg_type = j_msg.at("type"); - - if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) { - try { - olm::OlmMessage olm_msg = j_msg; - cache::client()->query_keys( - olm_msg.sender, - [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) { - if (e) { - nhlog::crypto()->error( - "Failed to query user keys, dropping olm " - "message"); - return; - } - handle_olm_message(std::move(olm_msg), userKeys); - }); - } catch (const nlohmann::json::exception &e) { - nhlog::crypto()->warn( - "parsing error for olm message: {} {}", e.what(), j_msg.dump(2)); - } catch (const std::invalid_argument &e) { - nhlog::crypto()->warn("validation error for olm message: {} {}", - e.what(), - j_msg.dump(2)); - } - - } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) { - nhlog::crypto()->warn("handling key request event: {}", j_msg.dump(2)); - try { - mtx::events::DeviceEvent req = j_msg; - if (req.content.action == mtx::events::msg::RequestAction::Request) - handle_key_request_message(req); - else - nhlog::crypto()->warn( - "ignore key request (unhandled action): {}", - req.content.request_id); - } catch (const nlohmann::json::exception &e) { - nhlog::crypto()->warn( - "parsing error for key_request message: {} {}", - e.what(), - j_msg.dump(2)); - } - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) { - auto message = std::get< - mtx::events::DeviceEvent>(msg); - ChatPage::instance()->receivedDeviceVerificationAccept(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) { - auto message = std::get< - mtx::events::DeviceEvent>(msg); - ChatPage::instance()->receivedDeviceVerificationRequest(message.content, - message.sender); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) { - auto message = std::get< - mtx::events::DeviceEvent>(msg); - ChatPage::instance()->receivedDeviceVerificationCancel(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) { - auto message = - std::get>( - msg); - ChatPage::instance()->receivedDeviceVerificationKey(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) { - auto message = - std::get>( - msg); - ChatPage::instance()->receivedDeviceVerificationMac(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) { - auto message = std::get< - mtx::events::DeviceEvent>(msg); - ChatPage::instance()->receivedDeviceVerificationStart(message.content, - message.sender); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) { - auto message = std::get< - mtx::events::DeviceEvent>(msg); - ChatPage::instance()->receivedDeviceVerificationReady(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) { - auto message = - std::get>( - msg); - ChatPage::instance()->receivedDeviceVerificationDone(message.content); - } else if (auto e = - std::get_if>( - &msg)) { - handle_secret_request(e, e->sender); - } else { - nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2)); - } - } -} - -void -handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys) -{ - nhlog::crypto()->info("sender : {}", msg.sender); - nhlog::crypto()->info("sender_key: {}", msg.sender_key); - - if (msg.sender_key == olm::client()->identity_keys().ed25519) { - nhlog::crypto()->warn("Ignoring olm message from ourselves!"); - return; - } - - const auto my_key = olm::client()->identity_keys().curve25519; - - bool failed_decryption = false; - - for (const auto &cipher : msg.ciphertext) { - // We skip messages not meant for the current device. - if (cipher.first != my_key) { - nhlog::crypto()->debug( - "Skipping message for {} since we are {}.", cipher.first, my_key); - continue; - } - - const auto type = cipher.second.type; - nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); - - auto payload = try_olm_decryption(msg.sender_key, cipher.second); - - if (payload.is_null()) { - // Check for PRE_KEY message - if (cipher.second.type == 0) { - payload = handle_pre_key_olm_message( - msg.sender, msg.sender_key, cipher.second); - } else { - nhlog::crypto()->error("Undecryptable olm message!"); - failed_decryption = true; - continue; - } - } - - if (!payload.is_null()) { - mtx::events::collections::DeviceEvents device_event; - - // Other properties are included in order to prevent an attacker from - // publishing someone else's curve25519 keys as their own and subsequently - // claiming to have sent messages which they didn't. sender must correspond - // to the user who sent the event, recipient to the local user, and - // recipient_keys to the local ed25519 key. - std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"]; - if (receiver_ed25519.empty() || - receiver_ed25519 != olm::client()->identity_keys().ed25519) { - nhlog::crypto()->warn( - "Decrypted event doesn't include our ed25519: {}", - payload.dump()); - return; - } - std::string receiver = payload["recipient"]; - if (receiver.empty() || receiver != http::client()->user_id().to_string()) { - nhlog::crypto()->warn( - "Decrypted event doesn't include our user_id: {}", - payload.dump()); - return; - } - - // Clients must confirm that the sender_key and the ed25519 field value - // under the keys property match the keys returned by /keys/query for the - // given user, and must also verify the signature of the payload. Without - // this check, a client cannot be sure that the sender device owns the - // private part of the ed25519 key it claims to have in the Olm payload. - // This is crucial when the ed25519 key corresponds to a verified device. - std::string sender_ed25519 = payload["keys"]["ed25519"]; - if (sender_ed25519.empty()) { - nhlog::crypto()->warn( - "Decrypted event doesn't include sender ed25519: {}", - payload.dump()); - return; - } - - bool from_their_device = false; - for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - auto c_key = key.keys.find("curve25519:" + device_id); - auto e_key = key.keys.find("ed25519:" + device_id); - - if (c_key == key.keys.end() || e_key == key.keys.end()) { - nhlog::crypto()->warn( - "Skipping device {} as we have no keys for it.", - device_id); - } else if (c_key->second == msg.sender_key && - e_key->second == sender_ed25519) { - from_their_device = true; - break; - } - } - if (!from_their_device) { - nhlog::crypto()->warn("Decrypted event isn't sent from a device " - "listed by that user! {}", - payload.dump()); - return; - } - - { - std::string msg_type = payload["type"]; - json event_array = json::array(); - event_array.push_back(payload); - - std::vector temp_events; - mtx::responses::utils::parse_device_events(event_array, - temp_events); - if (temp_events.empty()) { - nhlog::crypto()->warn("Decrypted unknown event: {}", - payload.dump()); - return; - } - device_event = temp_events.at(0); - } - - using namespace mtx::events; - if (auto e1 = - std::get_if>(&device_event)) { - ChatPage::instance()->receivedDeviceVerificationAccept(e1->content); - } else if (auto e2 = std::get_if>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationRequest(e2->content, - e2->sender); - } else if (auto e3 = std::get_if>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationCancel(e3->content); - } else if (auto e4 = std::get_if>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationKey(e4->content); - } else if (auto e5 = std::get_if>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationMac(e5->content); - } else if (auto e6 = std::get_if>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationStart(e6->content, - e6->sender); - } else if (auto e7 = std::get_if>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationReady(e7->content); - } else if (auto e8 = std::get_if>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationDone(e8->content); - } else if (auto roomKey = - std::get_if>(&device_event)) { - create_inbound_megolm_session( - *roomKey, msg.sender_key, sender_ed25519); - } else if (auto forwardedRoomKey = - std::get_if>( - &device_event)) { - forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back( - msg.sender_key); - import_inbound_megolm_session(*forwardedRoomKey); - } else if (auto e = - std::get_if>(&device_event)) { - auto local_user = http::client()->user_id(); - - if (msg.sender != local_user.to_string()) - return; - - auto secret_name = - request_id_to_secret_name.find(e->content.request_id); - - if (secret_name != request_id_to_secret_name.end()) { - nhlog::crypto()->info("Received secret: {}", - secret_name->second); - - mtx::events::msg::SecretRequest secretRequest{}; - secretRequest.action = - mtx::events::msg::RequestAction::Cancellation; - secretRequest.requesting_device_id = - http::client()->device_id(); - secretRequest.request_id = e->content.request_id; - - auto verificationStatus = - cache::verificationStatus(local_user.to_string()); - - if (!verificationStatus) - return; - - auto deviceKeys = cache::userKeys(local_user.to_string()); - std::string sender_device_id; - if (deviceKeys) { - for (auto &[dev, key] : deviceKeys->device_keys) { - if (key.keys["curve25519:" + dev] == - msg.sender_key) { - sender_device_id = dev; - break; - } - } - } - - std::map< - mtx::identifiers::User, - std::map> - body; - - for (const auto &dev : - verificationStatus->verified_devices) { - if (dev != secretRequest.requesting_device_id && - dev != sender_device_id) - body[local_user][dev] = secretRequest; - } - - http::client() - ->send_to_device( - http::client()->generate_txn_id(), - body, - [name = - secret_name->second](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error( - "Failed to send request cancellation " - "for secrect " - "'{}'", - name); - } - }); - - nhlog::crypto()->info("Storing secret {}", - secret_name->second); - cache::client()->storeSecret(secret_name->second, - e->content.secret); - - request_id_to_secret_name.erase(secret_name); - } - - } else if (auto sec_req = - std::get_if>(&device_event)) { - handle_secret_request(sec_req, msg.sender); - } - - return; - } else { - failed_decryption = true; - } - } - - if (failed_decryption) { - try { - std::map> targets; - for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - if (key.keys.at("curve25519:" + device_id) == msg.sender_key) - targets[msg.sender].push_back(device_id); - } - - send_encrypted_to_device_messages( - targets, mtx::events::DeviceEvent{}, true); - nhlog::crypto()->info("Recovering from broken olm channel with {}:{}", - msg.sender, - msg.sender_key); - } catch (std::exception &e) { - nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", - e.what()); - } - } -} - -nlohmann::json -handle_pre_key_olm_message(const std::string &sender, - const std::string &sender_key, - const mtx::events::msg::OlmCipherContent &content) -{ - nhlog::crypto()->info("opening olm session with {}", sender); - - mtx::crypto::OlmSessionPtr inbound_session = nullptr; - try { - inbound_session = - olm::client()->create_inbound_session_from(sender_key, content.body); - - // We also remove the one time key used to establish that - // session so we'll have to update our copy of the account object. - cache::saveOlmAccount(olm::client()->save("secret")); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to create inbound session with {}: {}", sender, e.what()); - return {}; - } - - if (!mtx::crypto::matches_inbound_session_from( - inbound_session.get(), sender_key, content.body)) { - nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})", - sender); - return {}; - } - - mtx::crypto::BinaryBuf output; - try { - output = - olm::client()->decrypt_message(inbound_session.get(), content.type, content.body); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to decrypt olm message {}: {}", content.body, e.what()); - return {}; - } - - auto plaintext = json::parse(std::string((char *)output.data(), output.size())); - nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2)); - - try { - nhlog::crypto()->debug("New olm session: {}", - mtx::crypto::session_id(inbound_session.get())); - cache::saveOlmSession( - sender_key, std::move(inbound_session), QDateTime::currentMSecsSinceEpoch()); - } catch (const lmdb::error &e) { - nhlog::db()->warn( - "failed to save inbound olm session from {}: {}", sender, e.what()); - } - - return plaintext; -} - -mtx::events::msg::Encrypted -encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body) -{ - using namespace mtx::events; - using namespace mtx::identifiers; - - auto own_user_id = http::client()->user_id().to_string(); - - auto members = cache::client()->getMembersWithKeys( - room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers()); - - std::map> sendSessionTo; - mtx::crypto::OutboundGroupSessionPtr session = nullptr; - GroupSessionData group_session_data; - - if (cache::outboundMegolmSessionExists(room_id)) { - auto res = cache::getOutboundMegolmSession(room_id); - auto encryptionSettings = cache::client()->roomEncryptionSettings(room_id); - mtx::events::state::Encryption defaultSettings; - - // rotate if we crossed the limits for this key - if (res.data.message_index < - encryptionSettings.value_or(defaultSettings).rotation_period_msgs && - (QDateTime::currentMSecsSinceEpoch() - res.data.timestamp) < - encryptionSettings.value_or(defaultSettings).rotation_period_ms) { - auto member_it = members.begin(); - auto session_member_it = res.data.currently.keys.begin(); - auto session_member_it_end = res.data.currently.keys.end(); - - while (member_it != members.end() || - session_member_it != session_member_it_end) { - if (member_it == members.end()) { - // a member left, purge session! - nhlog::crypto()->debug( - "Rotating megolm session because of left member"); - break; - } - - if (session_member_it == session_member_it_end) { - // share with all remaining members - while (member_it != members.end()) { - sendSessionTo[member_it->first] = {}; - - if (member_it->second) - for (const auto &dev : - member_it->second->device_keys) - if (member_it->first != - own_user_id || - dev.first != device_id) - sendSessionTo[member_it - ->first] - .push_back(dev.first); - - ++member_it; - } - - session = std::move(res.session); - break; - } - - if (member_it->first > session_member_it->first) { - // a member left, purge session - nhlog::crypto()->debug( - "Rotating megolm session because of left member"); - break; - } else if (member_it->first < session_member_it->first) { - // new member, send them the session at this index - sendSessionTo[member_it->first] = {}; - - if (member_it->second) { - for (const auto &dev : - member_it->second->device_keys) - if (member_it->first != own_user_id || - dev.first != device_id) - sendSessionTo[member_it->first] - .push_back(dev.first); - } - - ++member_it; - } else { - // compare devices - bool device_removed = false; - for (const auto &dev : - session_member_it->second.deviceids) { - if (!member_it->second || - !member_it->second->device_keys.count( - dev.first)) { - device_removed = true; - break; - } - } - - if (device_removed) { - // device removed, rotate session! - nhlog::crypto()->debug( - "Rotating megolm session because of removed " - "device of {}", - member_it->first); - break; - } - - // check for new devices to share with - if (member_it->second) - for (const auto &dev : - member_it->second->device_keys) - if (!session_member_it->second.deviceids - .count(dev.first) && - (member_it->first != own_user_id || - dev.first != device_id)) - sendSessionTo[member_it->first] - .push_back(dev.first); - - ++member_it; - ++session_member_it; - if (member_it == members.end() && - session_member_it == session_member_it_end) { - // all devices match or are newly added - session = std::move(res.session); - } - } - } - } - - group_session_data = std::move(res.data); - } - - if (!session) { - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(session.get()); - const auto session_key = mtx::crypto::session_key(session.get()); - - // Saving the new megolm session. - GroupSessionData session_data{}; - session_data.message_index = 0; - session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); - session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519; - - sendSessionTo.clear(); - - for (const auto &[user, devices] : members) { - sendSessionTo[user] = {}; - session_data.currently.keys[user] = {}; - if (devices) { - for (const auto &[device_id_, key] : devices->device_keys) { - (void)key; - if (device_id != device_id_ || user != own_user_id) { - sendSessionTo[user].push_back(device_id_); - session_data.currently.keys[user] - .deviceids[device_id_] = 0; - } - } - } - } - - { - MegolmSessionIndex index; - index.room_id = room_id; - index.session_id = session_id; - index.sender_key = olm::client()->identity_keys().curve25519; - auto megolm_session = - olm::client()->init_inbound_group_session(session_key); - cache::saveInboundMegolmSession( - index, std::move(megolm_session), session_data); - } - - cache::saveOutboundMegolmSession(room_id, session_data, session); - group_session_data = std::move(session_data); - } - - mtx::events::DeviceEvent megolm_payload{}; - megolm_payload.content.algorithm = MEGOLM_ALGO; - megolm_payload.content.room_id = room_id; - megolm_payload.content.session_id = mtx::crypto::session_id(session.get()); - megolm_payload.content.session_key = mtx::crypto::session_key(session.get()); - megolm_payload.type = mtx::events::EventType::RoomKey; - - if (!sendSessionTo.empty()) - olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); - - // relations shouldn't be encrypted... - mtx::common::Relations relations = mtx::common::parse_relations(body["content"]); - - auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); - - // Prepare the m.room.encrypted event. - msg::Encrypted data; - data.ciphertext = std::string((char *)payload.data(), payload.size()); - data.sender_key = olm::client()->identity_keys().curve25519; - data.session_id = mtx::crypto::session_id(session.get()); - data.device_id = device_id; - data.algorithm = MEGOLM_ALGO; - data.relations = relations; - - group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); - nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); - - // update current set of members for the session with the new members and that message_index - for (const auto &[user, devices] : sendSessionTo) { - if (!group_session_data.currently.keys.count(user)) - group_session_data.currently.keys[user] = {}; - - for (const auto &device_id_ : devices) { - if (!group_session_data.currently.keys[user].deviceids.count(device_id_)) - group_session_data.currently.keys[user].deviceids[device_id_] = - group_session_data.message_index; - } - } - - // We need to re-pickle the session after we send a message to save the new message_index. - cache::updateOutboundMegolmSession(room_id, group_session_data, session); - - return data; -} - -nlohmann::json -try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &msg) -{ - auto session_ids = cache::getOlmSessions(sender_key); - - nhlog::crypto()->info("attempt to decrypt message with {} known session_ids", - session_ids.size()); - - for (const auto &id : session_ids) { - auto session = cache::getOlmSession(sender_key, id); - - if (!session) { - nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id); - continue; - } - - mtx::crypto::BinaryBuf text; - - try { - text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); - nhlog::crypto()->debug("Updated olm session: {}", - mtx::crypto::session_id(session->get())); - cache::saveOlmSession( - id, std::move(session.value()), QDateTime::currentMSecsSinceEpoch()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}", - msg.type, - sender_key, - id, - e.what()); - continue; - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save session: {}", e.what()); - return {}; - } - - try { - return json::parse(std::string_view((char *)text.data(), text.size())); - } catch (const json::exception &e) { - nhlog::crypto()->critical( - "failed to parse the decrypted session msg: {} {}", - e.what(), - std::string_view((char *)text.data(), text.size())); - } - } - - return {}; -} - -void -create_inbound_megolm_session(const mtx::events::DeviceEvent &roomKey, - const std::string &sender_key, - const std::string &sender_ed25519) -{ - MegolmSessionIndex index; - index.room_id = roomKey.content.room_id; - index.session_id = roomKey.content.session_id; - index.sender_key = sender_key; - - try { - GroupSessionData data{}; - data.forwarding_curve25519_key_chain = {sender_key}; - data.sender_claimed_ed25519_key = sender_ed25519; - - auto megolm_session = - olm::client()->init_inbound_group_session(roomKey.content.session_key); - cache::saveInboundMegolmSession(index, std::move(megolm_session), data); - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what()); - return; - } - - nhlog::crypto()->info( - "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); - - ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); -} - -void -import_inbound_megolm_session( - const mtx::events::DeviceEvent &roomKey) -{ - MegolmSessionIndex index; - index.room_id = roomKey.content.room_id; - index.session_id = roomKey.content.session_id; - index.sender_key = roomKey.content.sender_key; - - try { - auto megolm_session = - olm::client()->import_inbound_group_session(roomKey.content.session_key); - - GroupSessionData data{}; - data.forwarding_curve25519_key_chain = - roomKey.content.forwarding_curve25519_key_chain; - data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key; - - cache::saveInboundMegolmSession(index, std::move(megolm_session), data); - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what()); - return; - } - - nhlog::crypto()->info( - "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); - - ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); -} - -void -mark_keys_as_published() -{ - olm::client()->mark_keys_as_published(); - cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); -} - -void -send_key_request_for(mtx::events::EncryptedEvent e, - const std::string &request_id, - bool cancel) -{ - using namespace mtx::events; - - nhlog::crypto()->debug("sending key request: sender_key {}, session_id {}", - e.content.sender_key, - e.content.session_id); - - mtx::events::msg::KeyRequest request; - request.action = cancel ? mtx::events::msg::RequestAction::Cancellation - : mtx::events::msg::RequestAction::Request; - - request.algorithm = MEGOLM_ALGO; - request.room_id = e.room_id; - request.sender_key = e.content.sender_key; - request.session_id = e.content.session_id; - request.request_id = request_id; - request.requesting_device_id = http::client()->device_id(); - - nhlog::crypto()->debug("m.room_key_request: {}", json(request).dump(2)); - - std::map> body; - body[mtx::identifiers::parse(e.sender)][e.content.device_id] = - request; - body[http::client()->user_id()]["*"] = request; - - http::client()->send_to_device( - http::client()->generate_txn_id(), body, [e](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - nhlog::net()->info("m.room_key_request sent to {}:{} and your own devices", - e.sender, - e.content.device_id); - }); -} - -void -handle_key_request_message(const mtx::events::DeviceEvent &req) -{ - if (req.content.algorithm != MEGOLM_ALGO) { - nhlog::crypto()->debug("ignoring key request {} with invalid algorithm: {}", - req.content.request_id, - req.content.algorithm); - return; - } - - // Check if we were the sender of the session being requested (unless it is actually us - // requesting the session). - if (req.sender != http::client()->user_id().to_string() && - req.content.sender_key != olm::client()->identity_keys().curve25519) { - nhlog::crypto()->debug( - "ignoring key request {} because we did not create the requested session: " - "\nrequested({}) ours({})", - req.content.request_id, - req.content.sender_key, - olm::client()->identity_keys().curve25519); - return; - } - - // Check that the requested session_id and the one we have saved match. - MegolmSessionIndex index{}; - index.room_id = req.content.room_id; - index.session_id = req.content.session_id; - index.sender_key = req.content.sender_key; - - // Check if we have the keys for the requested session. - auto sessionData = cache::getMegolmSessionData(index); - if (!sessionData) { - nhlog::crypto()->warn("requested session not found in room: {}", - req.content.room_id); - return; - } - - const auto session = cache::getInboundMegolmSession(index); - if (!session) { - nhlog::crypto()->warn("No session with id {} in db", req.content.session_id); - return; - } - - if (!cache::isRoomMember(req.sender, req.content.room_id)) { - nhlog::crypto()->warn( - "user {} that requested the session key is not member of the room {}", - req.sender, - req.content.room_id); - return; - } - - // check if device is verified - auto verificationStatus = cache::verificationStatus(req.sender); - bool verifiedDevice = false; - if (verificationStatus && - // Share keys, if the option to share with trusted users is enabled or with yourself - (ChatPage::instance()->userSettings()->shareKeysWithTrustedUsers() || - req.sender == http::client()->user_id().to_string())) { - for (const auto &dev : verificationStatus->verified_devices) { - if (dev == req.content.requesting_device_id) { - verifiedDevice = true; - nhlog::crypto()->debug("Verified device: {}", dev); - break; - } - } - } - - bool shouldSeeKeys = false; - uint64_t minimumIndex = -1; - if (sessionData->currently.keys.count(req.sender)) { - if (sessionData->currently.keys.at(req.sender) - .deviceids.count(req.content.requesting_device_id)) { - shouldSeeKeys = true; - minimumIndex = sessionData->currently.keys.at(req.sender) - .deviceids.at(req.content.requesting_device_id); - } - } - - if (!verifiedDevice && !shouldSeeKeys) { - nhlog::crypto()->debug("ignoring key request for room {}", req.content.room_id); - return; - } - - if (verifiedDevice) { - // share the minimum index we have - minimumIndex = -1; - } - - try { - auto session_key = mtx::crypto::export_session(session.get(), minimumIndex); - - // - // Prepare the m.room_key event. - // - mtx::events::msg::ForwardedRoomKey forward_key{}; - forward_key.algorithm = MEGOLM_ALGO; - forward_key.room_id = index.room_id; - forward_key.session_id = index.session_id; - forward_key.session_key = session_key; - forward_key.sender_key = index.sender_key; - - // TODO(Nico): Figure out if this is correct - forward_key.sender_claimed_ed25519_key = sessionData->sender_claimed_ed25519_key; - forward_key.forwarding_curve25519_key_chain = - sessionData->forwarding_curve25519_key_chain; - - send_megolm_key_to_device( - req.sender, req.content.requesting_device_id, forward_key); - } catch (std::exception &e) { - nhlog::crypto()->error("Failed to forward session key: {}", e.what()); - } -} - -void -send_megolm_key_to_device(const std::string &user_id, - const std::string &device_id, - const mtx::events::msg::ForwardedRoomKey &payload) -{ - mtx::events::DeviceEvent room_key; - room_key.content = payload; - room_key.type = mtx::events::EventType::ForwardedRoomKey; - - std::map> targets; - targets[user_id] = {device_id}; - send_encrypted_to_device_messages(targets, room_key); - nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id); -} - -DecryptionResult -decryptEvent(const MegolmSessionIndex &index, - const mtx::events::EncryptedEvent &event, - bool dont_write_db) -{ - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - return {DecryptionErrorCode::MissingSession, std::nullopt, std::nullopt}; - } - } catch (const lmdb::error &e) { - return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; - } - - // TODO: Lookup index,event_id,origin_server_ts tuple for replay attack errors - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto sessionData = - cache::client()->getMegolmSessionData(index).value_or(GroupSessionData{}); - - auto res = - olm::client()->decrypt_group_message(session.get(), event.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - - if (!event.event_id.empty() && event.event_id[0] == '$') { - auto oldIdx = sessionData.indices.find(res.message_index); - if (oldIdx != sessionData.indices.end()) { - if (oldIdx->second != event.event_id) - return {DecryptionErrorCode::ReplayAttack, - std::nullopt, - std::nullopt}; - } else if (!dont_write_db) { - sessionData.indices[res.message_index] = event.event_id; - cache::client()->saveInboundMegolmSession( - index, std::move(session), sessionData); - } - } - } catch (const lmdb::error &e) { - return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; - } catch (const mtx::crypto::olm_exception &e) { - if (e.error_code() == mtx::crypto::OlmErrorCode::UNKNOWN_MESSAGE_INDEX) - return {DecryptionErrorCode::MissingSessionIndex, e.what(), std::nullopt}; - return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt}; - } - - try { - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = event.event_id; - body["sender"] = event.sender; - body["origin_server_ts"] = event.origin_server_ts; - body["unsigned"] = event.unsigned_data; - - // relations are unencrypted in content... - mtx::common::add_relations(body["content"], event.content.relations); - - mtx::events::collections::TimelineEvent te; - mtx::events::collections::from_json(body, te); - - return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)}; - } catch (std::exception &e) { - return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt}; - } -} - -crypto::Trust -calculate_trust(const std::string &user_id, const std::string &curve25519) -{ - auto status = cache::client()->verificationStatus(user_id); - crypto::Trust trustlevel = crypto::Trust::Unverified; - if (status.verified_device_keys.count(curve25519)) - trustlevel = status.verified_device_keys.at(curve25519); - - return trustlevel; -} - -//! Send encrypted to device messages, targets is a map from userid to device ids or {} for all -//! devices -void -send_encrypted_to_device_messages(const std::map> targets, - const mtx::events::collections::DeviceEvents &event, - bool force_new_session) -{ - static QMap, qint64> rateLimit; - - nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event); - - std::map> keysToQuery; - mtx::requests::ClaimKeys claims; - std::map> - messages; - std::map> pks; - - auto our_curve = olm::client()->identity_keys().curve25519; - - for (const auto &[user, devices] : targets) { - auto deviceKeys = cache::client()->userKeys(user); - - // no keys for user, query them - if (!deviceKeys) { - keysToQuery[user] = devices; - continue; - } - - auto deviceTargets = devices; - if (devices.empty()) { - deviceTargets.clear(); - for (const auto &[device, keys] : deviceKeys->device_keys) { - (void)keys; - deviceTargets.push_back(device); - } - } - - for (const auto &device : deviceTargets) { - if (!deviceKeys->device_keys.count(device)) { - keysToQuery[user] = {}; - break; - } - - auto d = deviceKeys->device_keys.at(device); - - if (!d.keys.count("curve25519:" + device) || - !d.keys.count("ed25519:" + device)) { - nhlog::crypto()->warn("Skipping device {} since it has no keys!", - device); - continue; - } - - auto device_curve = d.keys.at("curve25519:" + device); - if (device_curve == our_curve) { - nhlog::crypto()->warn("Skipping our own device, since sending " - "ourselves olm messages makes no sense."); - continue; - } - - auto session = cache::getLatestOlmSession(device_curve); - if (!session || force_new_session) { - auto currentTime = QDateTime::currentSecsSinceEpoch(); - if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < - currentTime) { - claims.one_time_keys[user][device] = - mtx::crypto::SIGNED_CURVE25519; - pks[user][device].ed25519 = d.keys.at("ed25519:" + device); - pks[user][device].curve25519 = - d.keys.at("curve25519:" + device); - - rateLimit.insert(QPair(user, device), currentTime); - } else { - nhlog::crypto()->warn("Not creating new session with {}:{} " - "because of rate limit", - user, - device); - } - continue; - } - - messages[mtx::identifiers::parse(user)][device] = - olm::client() - ->create_olm_encrypted_content(session->get(), - ev_json, - UserId(user), - d.keys.at("ed25519:" + device), - device_curve) - .get(); - - try { - nhlog::crypto()->debug("Updated olm session: {}", - mtx::crypto::session_id(session->get())); - cache::saveOlmSession(d.keys.at("curve25519:" + device), - std::move(*session), - QDateTime::currentMSecsSinceEpoch()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", - e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to pickle outbound olm session: {}", e.what()); - } - } - } - - if (!messages.empty()) - http::client()->send_to_device( - http::client()->generate_txn_id(), messages, [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - }); - - auto BindPks = [ev_json](decltype(pks) pks_temp) { - return [pks = pks_temp, ev_json](const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr) { - std::map> - messages; - for (const auto &[user_id, retrieved_devices] : res.one_time_keys) { - nhlog::net()->debug("claimed keys for {}", user_id); - if (retrieved_devices.size() == 0) { - nhlog::net()->debug( - "no one-time keys found for user_id: {}", user_id); - continue; - } - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - - nhlog::net()->debug( - "{} : \n {}", device_id, rd.second.dump(2)); - - if (rd.second.empty() || - !rd.second.begin()->contains("key")) { - nhlog::net()->warn( - "Skipping device {} as it has no key.", - device_id); - continue; - } - - auto otk = rd.second.begin()->at("key"); - - auto sign_key = pks.at(user_id).at(device_id).ed25519; - auto id_key = pks.at(user_id).at(device_id).curve25519; - - // Verify signature - { - auto signedKey = *rd.second.begin(); - std::string signature = - signedKey["signatures"][user_id].value( - "ed25519:" + device_id, ""); - - if (signature.empty() || - !mtx::crypto::ed25519_verify_signature( - sign_key, signedKey, signature)) { - nhlog::net()->warn( - "Skipping device {} as its one time key " - "has an invalid signature.", - device_id); - continue; - } - } - - auto session = - olm::client()->create_outbound_session(id_key, otk); - - messages[mtx::identifiers::parse( - user_id)][device_id] = - olm::client() - ->create_olm_encrypted_content(session.get(), - ev_json, - UserId(user_id), - sign_key, - id_key) - .get(); - - try { - nhlog::crypto()->debug( - "Updated olm session: {}", - mtx::crypto::session_id(session.get())); - cache::saveOlmSession( - id_key, - std::move(session), - QDateTime::currentMSecsSinceEpoch()); - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save outbound olm session: {}", - e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to pickle outbound olm session: {}", - e.what()); - } - } - nhlog::net()->info("send_to_device: {}", user_id); - } - - if (!messages.empty()) - http::client()->send_to_device( - http::client()->generate_txn_id(), - messages, - [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - }); - }; - }; - - if (!claims.one_time_keys.empty()) - http::client()->claim_keys(claims, BindPks(pks)); - - if (!keysToQuery.empty()) { - mtx::requests::QueryKeys req; - req.device_keys = keysToQuery; - http::client()->query_keys( - req, - [ev_json, BindPks, our_curve](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - nhlog::net()->info("queried keys"); - - cache::client()->updateUserKeys(cache::nextBatchToken(), res); - - mtx::requests::ClaimKeys claim_keys; - - std::map> deviceKeys; - - for (const auto &user : res.device_keys) { - for (const auto &dev : user.second) { - const auto user_id = ::UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - if (user_id.get() == - http::client()->user_id().to_string() && - device_id.get() == http::client()->device_id()) - continue; - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - if (pks.curve25519 == our_curve) { - nhlog::crypto()->warn( - "Skipping our own device, since sending " - "ourselves olm messages makes no sense."); - continue; - } - - try { - if (!mtx::crypto::verify_identity_signature( - dev.second, device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto currentTime = QDateTime::currentSecsSinceEpoch(); - if (rateLimit.value(QPair(user.first, device_id.get())) + - 60 * 60 * 10 < - currentTime) { - deviceKeys[user_id].emplace(device_id, pks); - claim_keys.one_time_keys[user.first][device_id] = - mtx::crypto::SIGNED_CURVE25519; - - rateLimit.insert( - QPair(user.first, device_id.get()), - currentTime); - } else { - nhlog::crypto()->warn( - "Not creating new session with {}:{} " - "because of rate limit", - user.first, - device_id.get()); - continue; - } - - nhlog::net()->info("{}", device_id.get()); - nhlog::net()->info(" curve25519 {}", pks.curve25519); - nhlog::net()->info(" ed25519 {}", pks.ed25519); - } - } - - if (!claim_keys.one_time_keys.empty()) - http::client()->claim_keys(claim_keys, BindPks(deviceKeys)); - }); - } -} - -void -request_cross_signing_keys() -{ - mtx::events::msg::SecretRequest secretRequest{}; - secretRequest.action = mtx::events::msg::RequestAction::Request; - secretRequest.requesting_device_id = http::client()->device_id(); - - auto local_user = http::client()->user_id(); - - auto verificationStatus = cache::verificationStatus(local_user.to_string()); - - if (!verificationStatus) - return; - - auto request = [&](std::string secretName) { - secretRequest.name = secretName; - secretRequest.request_id = "ss." + http::client()->generate_txn_id(); - - request_id_to_secret_name[secretRequest.request_id] = secretRequest.name; - - std::map> - body; - - for (const auto &dev : verificationStatus->verified_devices) { - if (dev != secretRequest.requesting_device_id) - body[local_user][dev] = secretRequest; - } - - http::client()->send_to_device( - http::client()->generate_txn_id(), - body, - [request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("Failed to send request for secrect '{}'", - secretName); - // Cancel request on UI thread - QTimer::singleShot(1, cache::client(), [request_id]() { - request_id_to_secret_name.erase(request_id); - }); - return; - } - }); - - for (const auto &dev : verificationStatus->verified_devices) { - if (dev != secretRequest.requesting_device_id) - body[local_user][dev].action = - mtx::events::msg::RequestAction::Cancellation; - } - - // timeout after 15 min - QTimer::singleShot(15 * 60 * 1000, [secretRequest, body]() { - if (request_id_to_secret_name.count(secretRequest.request_id)) { - request_id_to_secret_name.erase(secretRequest.request_id); - http::client()->send_to_device( - http::client()->generate_txn_id(), - body, - [secretRequest](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error( - "Failed to cancel request for secrect '{}'", - secretRequest.name); - return; - } - }); - } - }); - }; - - request(mtx::secret_storage::secrets::cross_signing_self_signing); - request(mtx::secret_storage::secrets::cross_signing_user_signing); - request(mtx::secret_storage::secrets::megolm_backup_v1); -} - -namespace { -void -unlock_secrets(const std::string &key, - const std::map &secrets) -{ - http::client()->secret_storage_key( - key, - [secrets](mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("Failed to download secret storage key"); - return; - } - - emit ChatPage::instance()->downloadedSecrets(keyDesc, secrets); - }); -} -} - -void -download_cross_signing_keys() -{ - using namespace mtx::secret_storage; - http::client()->secret_storage_secret( - secrets::megolm_backup_v1, [](Secret secret, mtx::http::RequestErr err) { - std::optional backup_key; - if (!err) - backup_key = secret; - - http::client()->secret_storage_secret( - secrets::cross_signing_self_signing, - [backup_key](Secret secret, mtx::http::RequestErr err) { - std::optional self_signing_key; - if (!err) - self_signing_key = secret; - - http::client()->secret_storage_secret( - secrets::cross_signing_user_signing, - [backup_key, self_signing_key](Secret secret, - mtx::http::RequestErr err) { - std::optional user_signing_key; - if (!err) - user_signing_key = secret; - - std::map> - secrets; - - if (backup_key && !backup_key->encrypted.empty()) - secrets[backup_key->encrypted.begin()->first] - [secrets::megolm_backup_v1] = - backup_key->encrypted.begin()->second; - if (self_signing_key && !self_signing_key->encrypted.empty()) - secrets[self_signing_key->encrypted.begin()->first] - [secrets::cross_signing_self_signing] = - self_signing_key->encrypted.begin()->second; - if (user_signing_key && !user_signing_key->encrypted.empty()) - secrets[user_signing_key->encrypted.begin()->first] - [secrets::cross_signing_user_signing] = - user_signing_key->encrypted.begin()->second; - - for (const auto &[key, secrets] : secrets) - unlock_secrets(key, secrets); - }); - }); - }); -} - -} // namespace olm diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 25262c59..ff93f7d8 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -16,116 +16,115 @@ ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject , event_id_{event_id} , room_id_{room_id} { - try { - addUsers(cache::readReceipts(event_id_, room_id_)); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id_.toStdString(), - room_id_.toStdString()); + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); - return; - } + return; + } - connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update); + connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update); } void ReadReceiptsModel::update() { - try { - addUsers(cache::readReceipts(event_id_, room_id_)); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id_.toStdString(), - room_id_.toStdString()); + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); - return; - } + return; + } } QHash ReadReceiptsModel::roleNames() const { - // Note: RawTimestamp is purposely not included here - return { - {Mxid, "mxid"}, - {DisplayName, "displayName"}, - {AvatarUrl, "avatarUrl"}, - {Timestamp, "timestamp"}, - }; + // Note: RawTimestamp is purposely not included here + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Timestamp, "timestamp"}, + }; } QVariant ReadReceiptsModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0) - return {}; + if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0) + return {}; - switch (role) { - case Mxid: - return readReceipts_[index.row()].first; - case DisplayName: - return cache::displayName(room_id_, readReceipts_[index.row()].first); - case AvatarUrl: - return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); - case Timestamp: - return dateFormat(readReceipts_[index.row()].second); - case RawTimestamp: - return readReceipts_[index.row()].second; - default: - return {}; - } + switch (role) { + case Mxid: + return readReceipts_[index.row()].first; + case DisplayName: + return cache::displayName(room_id_, readReceipts_[index.row()].first); + case AvatarUrl: + return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); + case Timestamp: + return dateFormat(readReceipts_[index.row()].second); + case RawTimestamp: + return readReceipts_[index.row()].second; + default: + return {}; + } } void ReadReceiptsModel::addUsers( const std::multimap> &users) { - auto newReceipts = users.size() - readReceipts_.size(); + auto newReceipts = users.size() - readReceipts_.size(); - if (newReceipts > 0) { - beginInsertRows( - QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1); + if (newReceipts > 0) { + beginInsertRows( + QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1); - for (const auto &user : users) { - QPair item = { - QString::fromStdString(user.second), - QDateTime::fromMSecsSinceEpoch(user.first)}; - if (!readReceipts_.contains(item)) - readReceipts_.push_back(item); - } - - endInsertRows(); + for (const auto &user : users) { + QPair item = {QString::fromStdString(user.second), + QDateTime::fromMSecsSinceEpoch(user.first)}; + if (!readReceipts_.contains(item)) + readReceipts_.push_back(item); } + + endInsertRows(); + } } QString ReadReceiptsModel::dateFormat(const QDateTime &then) const { - auto now = QDateTime::currentDateTime(); - auto days = then.daysTo(now); - - if (days == 0) - return QLocale::system().toString(then.time(), QLocale::ShortFormat); - else if (days < 2) - return tr("Yesterday, %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 7) - //: %1 is the name of the current day, %2 is the time the read receipt was read. The - //: result may look like this: Monday, 7:15 - return QString("%1, %2") - .arg(then.toString("dddd")) - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + auto now = QDateTime::currentDateTime(); + auto days = then.daysTo(now); + if (days == 0) return QLocale::system().toString(then.time(), QLocale::ShortFormat); + else if (days < 2) + return tr("Yesterday, %1") + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + else if (days < 7) + //: %1 is the name of the current day, %2 is the time the read receipt was read. The + //: result may look like this: Monday, 7:15 + return QString("%1, %2") + .arg(then.toString("dddd")) + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + + return QLocale::system().toString(then.time(), QLocale::ShortFormat); } ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent) : QSortFilterProxyModel{parent} , model_{event_id, room_id, this} { - setSourceModel(&model_); - setSortRole(ReadReceiptsModel::RawTimestamp); - sort(0, Qt::DescendingOrder); - setDynamicSortFilter(true); + setSourceModel(&model_); + setSortRole(ReadReceiptsModel::RawTimestamp); + sort(0, Qt::DescendingOrder); + setDynamicSortFilter(true); } diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index 3b45716c..7487fff8 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -13,61 +13,61 @@ class ReadReceiptsModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT public: - enum Roles - { - Mxid, - DisplayName, - AvatarUrl, - Timestamp, - RawTimestamp, - }; + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + Timestamp, + RawTimestamp, + }; - explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); + explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); - QString eventId() const { return event_id_; } - QString roomId() const { return room_id_; } + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } - QHash roleNames() const override; - int rowCount(const QModelIndex &parent) const override - { - Q_UNUSED(parent) - return readReceipts_.size(); - } - QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + int rowCount(const QModelIndex &parent) const override + { + Q_UNUSED(parent) + return readReceipts_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; public slots: - void addUsers(const std::multimap> &users); - void update(); + void addUsers(const std::multimap> &users); + void update(); private: - QString dateFormat(const QDateTime &then) const; + QString dateFormat(const QDateTime &then) const; - QString event_id_; - QString room_id_; - QVector> readReceipts_; + QString event_id_; + QString room_id_; + QVector> readReceipts_; }; class ReadReceiptsProxy : public QSortFilterProxyModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QString eventId READ eventId CONSTANT) - Q_PROPERTY(QString roomId READ roomId CONSTANT) + Q_PROPERTY(QString eventId READ eventId CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) public: - explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr); + explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr); - QString eventId() const { return event_id_; } - QString roomId() const { return room_id_; } + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } private: - QString event_id_; - QString room_id_; + QString event_id_; + QString room_id_; - ReadReceiptsModel model_; + ReadReceiptsModel model_; }; #endif // READRECEIPTSMODEL_H diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp index fb6a1b97..271a7fc2 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp @@ -23,6 +23,7 @@ #include "ui/FlatButton.h" #include "ui/RaisedButton.h" #include "ui/TextField.h" +#include "ui/UIA.h" #include "dialogs/FallbackAuth.h" #include "dialogs/ReCaptcha.h" @@ -33,496 +34,379 @@ Q_DECLARE_METATYPE(mtx::user_interactive::Auth) RegisterPage::RegisterPage(QWidget *parent) : QWidget(parent) { - qRegisterMetaType(); - qRegisterMetaType(); - top_layout_ = new QVBoxLayout(); + qRegisterMetaType(); + qRegisterMetaType(); + top_layout_ = new QVBoxLayout(); - back_layout_ = new QHBoxLayout(); - back_layout_->setSpacing(0); - back_layout_->setContentsMargins(5, 5, -1, -1); + back_layout_ = new QHBoxLayout(); + back_layout_->setSpacing(0); + back_layout_->setContentsMargins(5, 5, -1, -1); - back_button_ = new FlatButton(this); - back_button_->setMinimumSize(QSize(30, 30)); + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - back_button_->setIcon(icon); - back_button_->setIconSize(QSize(32, 32)); + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(32, 32)); - back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - back_layout_->addStretch(1); + back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + back_layout_->addStretch(1); - QIcon logo; - logo.addFile(":/logos/register.png"); + QIcon logo; + logo.addFile(":/logos/register.png"); - logo_ = new QLabel(this); - logo_->setPixmap(logo.pixmap(128)); + logo_ = new QLabel(this); + logo_->setPixmap(logo.pixmap(128)); - logo_layout_ = new QHBoxLayout(); - logo_layout_->setMargin(0); - logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + logo_layout_ = new QHBoxLayout(); + logo_layout_->setMargin(0); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); - form_wrapper_ = new QHBoxLayout(); - form_widget_ = new QWidget(); - form_widget_->setMinimumSize(QSize(350, 300)); + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 300)); - form_layout_ = new QVBoxLayout(); - form_layout_->setSpacing(20); - form_layout_->setContentsMargins(0, 0, 0, 40); - form_widget_->setLayout(form_layout_); + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 0, 0, 40); + form_widget_->setLayout(form_layout_); - form_wrapper_->addStretch(1); - form_wrapper_->addWidget(form_widget_); - form_wrapper_->addStretch(1); + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); - username_input_ = new TextField(); - username_input_->setLabel(tr("Username")); - username_input_->setRegexp(QRegularExpression("[a-z0-9._=/-]+")); - username_input_->setToolTip(tr("The username must not be empty, and must contain only the " - "characters a-z, 0-9, ., _, =, -, and /.")); + username_input_ = new TextField(); + username_input_->setLabel(tr("Username")); + username_input_->setRegexp(QRegularExpression("[a-z0-9._=/-]+")); + username_input_->setToolTip(tr("The username must not be empty, and must contain only the " + "characters a-z, 0-9, ., _, =, -, and /.")); - password_input_ = new TextField(); - password_input_->setLabel(tr("Password")); - password_input_->setRegexp(QRegularExpression("^.{8,}$")); - password_input_->setEchoMode(QLineEdit::Password); - password_input_->setToolTip(tr("Please choose a secure password. The exact requirements " - "for password strength may depend on your server.")); + password_input_ = new TextField(); + password_input_->setLabel(tr("Password")); + password_input_->setRegexp(QRegularExpression("^.{8,}$")); + password_input_->setEchoMode(QLineEdit::Password); + password_input_->setToolTip(tr("Please choose a secure password. The exact requirements " + "for password strength may depend on your server.")); - password_confirmation_ = new TextField(); - password_confirmation_->setLabel(tr("Password confirmation")); - password_confirmation_->setEchoMode(QLineEdit::Password); + password_confirmation_ = new TextField(); + password_confirmation_->setLabel(tr("Password confirmation")); + password_confirmation_->setEchoMode(QLineEdit::Password); - server_input_ = new TextField(); - server_input_->setLabel(tr("Homeserver")); - server_input_->setRegexp(QRegularExpression(".+")); - server_input_->setToolTip( - tr("A server that allows registration. Since matrix is decentralized, you need to first " - "find a server you can register on or host your own.")); + server_input_ = new TextField(); + server_input_->setLabel(tr("Homeserver")); + server_input_->setRegexp(QRegularExpression(".+")); + server_input_->setToolTip( + tr("A server that allows registration. Since matrix is decentralized, you need to first " + "find a server you can register on or host your own.")); - error_username_label_ = new QLabel(this); - error_username_label_->setWordWrap(true); - error_username_label_->hide(); + error_username_label_ = new QLabel(this); + error_username_label_->setWordWrap(true); + error_username_label_->hide(); - error_password_label_ = new QLabel(this); - error_password_label_->setWordWrap(true); - error_password_label_->hide(); + error_password_label_ = new QLabel(this); + error_password_label_->setWordWrap(true); + error_password_label_->hide(); - error_password_confirmation_label_ = new QLabel(this); - error_password_confirmation_label_->setWordWrap(true); - error_password_confirmation_label_->hide(); + error_password_confirmation_label_ = new QLabel(this); + error_password_confirmation_label_->setWordWrap(true); + error_password_confirmation_label_->hide(); - error_server_label_ = new QLabel(this); - error_server_label_->setWordWrap(true); - error_server_label_->hide(); + error_server_label_ = new QLabel(this); + error_server_label_->setWordWrap(true); + error_server_label_->hide(); - form_layout_->addWidget(username_input_, Qt::AlignHCenter); - form_layout_->addWidget(error_username_label_, Qt::AlignHCenter); - form_layout_->addWidget(password_input_, Qt::AlignHCenter); - form_layout_->addWidget(error_password_label_, Qt::AlignHCenter); - form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter); - form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter); - form_layout_->addWidget(server_input_, Qt::AlignHCenter); - form_layout_->addWidget(error_server_label_, Qt::AlignHCenter); + form_layout_->addWidget(username_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_username_label_, Qt::AlignHCenter); + form_layout_->addWidget(password_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_password_label_, Qt::AlignHCenter); + form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter); + form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter); + form_layout_->addWidget(server_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_server_label_, Qt::AlignHCenter); - button_layout_ = new QHBoxLayout(); - button_layout_->setSpacing(0); - button_layout_->setMargin(0); + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setMargin(0); - error_label_ = new QLabel(this); - error_label_->setWordWrap(true); + error_label_ = new QLabel(this); + error_label_->setWordWrap(true); - register_button_ = new RaisedButton(tr("REGISTER"), this); - register_button_->setMinimumSize(350, 65); - register_button_->setFontSize(conf::btn::fontSize); - register_button_->setCornerRadius(conf::btn::cornerRadius); + register_button_ = new RaisedButton(tr("REGISTER"), this); + register_button_->setMinimumSize(350, 65); + register_button_->setFontSize(conf::btn::fontSize); + register_button_->setCornerRadius(conf::btn::cornerRadius); - button_layout_->addStretch(1); - button_layout_->addWidget(register_button_); - button_layout_->addStretch(1); + button_layout_->addStretch(1); + button_layout_->addWidget(register_button_); + button_layout_->addStretch(1); - top_layout_->addLayout(back_layout_); - top_layout_->addLayout(logo_layout_); - top_layout_->addLayout(form_wrapper_); - top_layout_->addStretch(1); - top_layout_->addLayout(button_layout_); - top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); - top_layout_->addStretch(1); - setLayout(top_layout_); + top_layout_->addLayout(back_layout_); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); + setLayout(top_layout_); - connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); - connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); - connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername); - connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword); - connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_confirmation_, - &TextField::editingFinished, - this, - &RegisterPage::checkPasswordConfirmation); - connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer); + connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername); + connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword); + connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_confirmation_, + &TextField::editingFinished, + this, + &RegisterPage::checkPasswordConfirmation); + connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer); - connect( - this, - &RegisterPage::serverError, - this, - [this](const QString &msg) { - server_input_->setValid(false); - showError(error_server_label_, msg); - }, - Qt::QueuedConnection); + connect( + this, + &RegisterPage::serverError, + this, + [this](const QString &msg) { + server_input_->setValid(false); + showError(error_server_label_, msg); + }, + Qt::QueuedConnection); - connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup); - connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck); - connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration); - connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA); - connect( - this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth); + connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup); + connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck); + connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration); } void RegisterPage::onBackButtonClicked() { - emit backButtonClicked(); + emit backButtonClicked(); } void RegisterPage::showError(const QString &msg) { - emit errorOccurred(); - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - error_label_->setFixedHeight(qCeil(width / 200.0) * height); - error_label_->setText(msg); + emit errorOccurred(); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + error_label_->setFixedHeight(qCeil(width / 200.0) * height); + error_label_->setText(msg); } void RegisterPage::showError(QLabel *label, const QString &msg) { - emit errorOccurred(); - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - label->setFixedHeight((int)qCeil(width / 200.0) * height); - label->setText(msg); - label->show(); + emit errorOccurred(); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + label->setFixedHeight((int)qCeil(width / 200.0) * height); + label->setText(msg); + label->show(); } bool RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg) { - if (t_field->isValid()) { - label->hide(); - return true; - } else { - showError(label, msg); - return false; - } + if (t_field->isValid()) { + label->hide(); + return true; + } else { + showError(label, msg); + return false; + } } bool RegisterPage::checkUsername() { - return checkOneField(error_username_label_, - username_input_, - tr("The username must not be empty, and must contain only the " - "characters a-z, 0-9, ., _, =, -, and /.")); + return checkOneField(error_username_label_, + username_input_, + tr("The username must not be empty, and must contain only the " + "characters a-z, 0-9, ., _, =, -, and /.")); } bool RegisterPage::checkPassword() { - return checkOneField( - error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)")); + return checkOneField( + error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)")); } bool RegisterPage::checkPasswordConfirmation() { - if (password_input_->text() == password_confirmation_->text()) { - error_password_confirmation_label_->hide(); - password_confirmation_->setValid(true); - return true; - } else { - showError(error_password_confirmation_label_, tr("Passwords don't match")); - password_confirmation_->setValid(false); - return false; - } + if (password_input_->text() == password_confirmation_->text()) { + error_password_confirmation_label_->hide(); + password_confirmation_->setValid(true); + return true; + } else { + showError(error_password_confirmation_label_, tr("Passwords don't match")); + password_confirmation_->setValid(false); + return false; + } } bool RegisterPage::checkServer() { - // This doesn't check that the server is reachable, - // just that the input is not obviously wrong. - return checkOneField(error_server_label_, server_input_, tr("Invalid server name")); + // This doesn't check that the server is reachable, + // just that the input is not obviously wrong. + return checkOneField(error_server_label_, server_input_, tr("Invalid server name")); } void RegisterPage::onRegisterButtonClicked() { - if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) { - auto server = server_input_->text().toStdString(); + if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) { + auto server = server_input_->text().toStdString(); - http::client()->set_server(server); - http::client()->verify_certificates( - !UserSettings::instance()->disableCertificateValidation()); + http::client()->set_server(server); + http::client()->verify_certificates( + !UserSettings::instance()->disableCertificateValidation()); - // This starts a chain of `emit`s which ends up doing the - // registration. Signals are used rather than normal function - // calls so that the dialogs used in UIA work correctly. - // - // The sequence of events looks something like this: - // - // dowellKnownLookup - // v - // doVersionsCheck - // v - // doRegistration - // v - // doUIA <-----------------+ - // v | More auth required - // doRegistrationWithAuth -+ - // | Success - // v - // registering + // This starts a chain of `emit`s which ends up doing the + // registration. Signals are used rather than normal function + // calls so that the dialogs used in UIA work correctly. + // + // The sequence of events looks something like this: + // + // doKnownLookup + // v + // doVersionsCheck + // v + // doRegistration -> loops the UIAHandler until complete - emit wellKnownLookup(); + emit wellKnownLookup(); - emit registering(); - } + emit registering(); + } } void RegisterPage::doWellKnownLookup() { - http::client()->well_known( - [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - nhlog::net()->info("Autodiscovery: No .well-known."); - // Check that the homeserver can be reached - emit versionsCheck(); - return; - } - - if (!err->parse_error.empty()) { - emit serverError( - tr("Autodiscovery failed. Received malformed response.")); - nhlog::net()->error( - "Autodiscovery failed. Received malformed response."); - return; - } - - emit serverError(tr("Autodiscovery failed. Unknown error when " - "requesting .well-known.")); - nhlog::net()->error("Autodiscovery failed. Unknown error when " - "requesting .well-known. {} {}", - err->status_code, - err->error_code); - return; - } - - nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); - http::client()->set_server(res.homeserver.base_url); + http::client()->well_known( + [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + nhlog::net()->info("Autodiscovery: No .well-known."); // Check that the homeserver can be reached emit versionsCheck(); - }); + return; + } + + if (!err->parse_error.empty()) { + emit serverError(tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error("Autodiscovery failed. Received malformed response."); + return; + } + + emit serverError(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known. {} {}", + err->status_code, + err->error_code); + return; + } + + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); + http::client()->set_server(res.homeserver.base_url); + // Check that the homeserver can be reached + emit versionsCheck(); + }); } void RegisterPage::doVersionsCheck() { - // Make a request to /_matrix/client/versions to check the address - // given is a Matrix homeserver. - http::client()->versions( - [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - emit serverError( - tr("The required endpoints were not found. Possibly " - "not a Matrix server.")); - return; - } + // Make a request to /_matrix/client/versions to check the address + // given is a Matrix homeserver. + http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + emit serverError(tr("The required endpoints were not found. Possibly " + "not a Matrix server.")); + return; + } - if (!err->parse_error.empty()) { - emit serverError( - tr("Received malformed response. Make sure the homeserver " - "domain is valid.")); - return; - } + if (!err->parse_error.empty()) { + emit serverError(tr("Received malformed response. Make sure the homeserver " + "domain is valid.")); + return; + } - emit serverError(tr("An unknown error occured. Make sure the " - "homeserver domain is valid.")); - return; - } + emit serverError(tr("An unknown error occured. Make sure the " + "homeserver domain is valid.")); + return; + } - // Attempt registration without an `auth` dict - emit registration(); - }); + // Attempt registration without an `auth` dict + emit registration(); + }); } void RegisterPage::doRegistration() { - // These inputs should still be alright, but check just in case - if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { - auto username = username_input_->text().toStdString(); - auto password = password_input_->text().toStdString(); - http::client()->registration(username, password, registrationCb()); - } -} - -void -RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth) -{ - // These inputs should still be alright, but check just in case - if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { - auto username = username_input_->text().toStdString(); - auto password = password_input_->text().toStdString(); - http::client()->registration(username, password, auth, registrationCb()); - } + // These inputs should still be alright, but check just in case + if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + connect(UIA::instance(), &UIA::error, this, [this](QString msg) { + showError(msg); + disconnect(UIA::instance(), &UIA::error, this, nullptr); + }); + http::client()->registration( + username, password, ::UIA::instance()->genericHandler("Registration"), registrationCb()); + } } mtx::http::Callback RegisterPage::registrationCb() { - // Return a function to be used as the callback when an attempt at - // registration is made. - return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - emit registerOk(); - return; - } - - // The server requires registration flows. - if (err->status_code == 401) { - if (err->matrix_error.unauthorized.flows.empty()) { - nhlog::net()->warn("failed to retrieve registration flows: " - "status_code({}), matrix_error({}) ", - static_cast(err->status_code), - err->matrix_error.error); - showError(QString::fromStdString(err->matrix_error.error)); - return; - } - - // Attempt to complete a UIA stage - emit UIA(err->matrix_error.unauthorized); - return; - } - - nhlog::net()->error("failed to register: status_code ({}), matrix_error({})", - static_cast(err->status_code), - err->matrix_error.error); + // Return a function to be used as the callback when an attempt at + // registration is made. + return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + emit registerOk(); + disconnect(UIA::instance(), &UIA::error, this, nullptr); + return; + } + // The server requires registration flows. + if (err->status_code == 401) { + if (err->matrix_error.unauthorized.flows.empty()) { + nhlog::net()->warn("failed to retrieve registration flows: " + "status_code({}), matrix_error({}) ", + static_cast(err->status_code), + err->matrix_error.error); showError(QString::fromStdString(err->matrix_error.error)); - }; -} - -void -RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized) -{ - auto completed_stages = unauthorized.completed; - auto flows = unauthorized.flows; - auto session = - unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session; - - nhlog::ui()->info("Completed stages: {}", completed_stages.size()); - - if (!completed_stages.empty()) { - // Get rid of all flows which don't start with the sequence of - // stages that have already been completed. - flows.erase( - std::remove_if(flows.begin(), - flows.end(), - [completed_stages](auto flow) { - if (completed_stages.size() > flow.stages.size()) - return true; - for (size_t f = 0; f < completed_stages.size(); f++) - if (completed_stages[f] != flow.stages[f]) - return true; - return false; - }), - flows.end()); + } + return; } - if (flows.empty()) { - nhlog::ui()->error("No available registration flows!"); - showError(tr("No supported registration flows!")); - return; - } + nhlog::net()->error("failed to register: status_code ({}), matrix_error({})", + static_cast(err->status_code), + err->matrix_error.error); - auto current_stage = flows.front().stages.at(completed_stages.size()); - - if (current_stage == mtx::user_interactive::auth_types::recaptcha) { - auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this); - - connect(captchaDialog, - &dialogs::ReCaptcha::confirmation, - this, - [this, session, captchaDialog]() { - captchaDialog->close(); - captchaDialog->deleteLater(); - doRegistrationWithAuth(mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); - - connect( - captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred); - - QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); }); - - } else if (current_stage == mtx::user_interactive::auth_types::dummy) { - doRegistrationWithAuth( - mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}}); - - } else if (current_stage == mtx::user_interactive::auth_types::registration_token) { - bool ok; - QString token = - QInputDialog::getText(this, - tr("Registration token"), - tr("Please enter a valid registration token."), - QLineEdit::Normal, - QString(), - &ok); - - if (ok) { - emit registrationWithAuth(mtx::user_interactive::Auth{ - session, - mtx::user_interactive::auth::RegistrationToken{token.toStdString()}}); - } else { - emit errorOccurred(); - } - } else { - // use fallback - auto dialog = new dialogs::FallbackAuth( - QString::fromStdString(current_stage), QString::fromStdString(session), this); - - connect( - dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() { - dialog->close(); - dialog->deleteLater(); - emit registrationWithAuth(mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); - - connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred); - - dialog->show(); - } + showError(QString::fromStdString(err->matrix_error.error)); + }; } void RegisterPage::paintEvent(QPaintEvent *) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } diff --git a/src/RegisterPage.h b/src/RegisterPage.h index 42ea00cb..0d7da9ad 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h @@ -21,76 +21,72 @@ class QHBoxLayout; class RegisterPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - RegisterPage(QWidget *parent = nullptr); + RegisterPage(QWidget *parent = nullptr); protected: - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; signals: - void backButtonClicked(); - void errorOccurred(); + void backButtonClicked(); + void errorOccurred(); - //! Used to trigger the corresponding slot outside of the main thread. - void serverError(const QString &err); + //! Used to trigger the corresponding slot outside of the main thread. + void serverError(const QString &err); - void wellKnownLookup(); - void versionsCheck(); - void registration(); - void UIA(const mtx::user_interactive::Unauthorized &unauthorized); - void registrationWithAuth(const mtx::user_interactive::Auth &auth); + void wellKnownLookup(); + void versionsCheck(); + void registration(); - void registering(); - void registerOk(); + void registering(); + void registerOk(); private slots: - void onBackButtonClicked(); - void onRegisterButtonClicked(); + void onBackButtonClicked(); + void onRegisterButtonClicked(); - // function for showing different errors - void showError(const QString &msg); - void showError(QLabel *label, const QString &msg); + // function for showing different errors + void showError(const QString &msg); + void showError(QLabel *label, const QString &msg); - bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); - bool checkUsername(); - bool checkPassword(); - bool checkPasswordConfirmation(); - bool checkServer(); + bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); + bool checkUsername(); + bool checkPassword(); + bool checkPasswordConfirmation(); + bool checkServer(); - void doWellKnownLookup(); - void doVersionsCheck(); - void doRegistration(); - void doUIA(const mtx::user_interactive::Unauthorized &unauthorized); - void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth); - mtx::http::Callback registrationCb(); + void doWellKnownLookup(); + void doVersionsCheck(); + void doRegistration(); + mtx::http::Callback registrationCb(); private: - QVBoxLayout *top_layout_; + QVBoxLayout *top_layout_; - QHBoxLayout *back_layout_; - QHBoxLayout *logo_layout_; - QHBoxLayout *button_layout_; + QHBoxLayout *back_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; - QLabel *logo_; - QLabel *error_label_; - QLabel *error_username_label_; - QLabel *error_password_label_; - QLabel *error_password_confirmation_label_; - QLabel *error_server_label_; - QLabel *error_registration_token_label_; + QLabel *logo_; + QLabel *error_label_; + QLabel *error_username_label_; + QLabel *error_password_label_; + QLabel *error_password_confirmation_label_; + QLabel *error_server_label_; + QLabel *error_registration_token_label_; - FlatButton *back_button_; - RaisedButton *register_button_; + FlatButton *back_button_; + RaisedButton *register_button_; - QWidget *form_widget_; - QHBoxLayout *form_wrapper_; - QVBoxLayout *form_layout_; + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; - TextField *username_input_; - TextField *password_input_; - TextField *password_confirmation_; - TextField *server_input_; - TextField *registration_token_input_; + TextField *username_input_; + TextField *password_input_; + TextField *password_confirmation_; + TextField *server_input_; + TextField *registration_token_input_; }; diff --git a/src/RoomDirectoryModel.cpp b/src/RoomDirectoryModel.cpp new file mode 100644 index 00000000..707571d6 --- /dev/null +++ b/src/RoomDirectoryModel.cpp @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RoomDirectoryModel.h" +#include "Cache.h" +#include "ChatPage.h" + +#include + +RoomDirectoryModel::RoomDirectoryModel(QObject *parent, const std::string &server) + : QAbstractListModel(parent) + , server_(server) +{ + connect(ChatPage::instance(), &ChatPage::newRoom, this, [this](const QString &roomid) { + auto roomid_ = roomid.toStdString(); + + int i = 0; + for (const auto &room : publicRoomsData_) { + if (room.room_id == roomid_) { + emit dataChanged(index(i), index(i), {Roles::CanJoin}); + break; + } + i++; + } + }); + + connect(this, + &RoomDirectoryModel::fetchedRoomsBatch, + this, + &RoomDirectoryModel::displayRooms, + Qt::QueuedConnection); +} + +QHash +RoomDirectoryModel::roleNames() const +{ + return { + {Roles::Name, "name"}, + {Roles::Id, "roomid"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::Topic, "topic"}, + {Roles::MemberCount, "numMembers"}, + {Roles::Previewable, "canPreview"}, + {Roles::CanJoin, "canJoin"}, + }; +} + +void +RoomDirectoryModel::resetDisplayedData() +{ + beginResetModel(); + + prevBatch_ = ""; + nextBatch_ = ""; + canFetchMore_ = true; + + publicRoomsData_.clear(); + + endResetModel(); +} + +void +RoomDirectoryModel::setMatrixServer(const QString &s) +{ + server_ = s.toStdString(); + + nhlog::ui()->debug("Received matrix server: {}", server_); + + resetDisplayedData(); +} + +void +RoomDirectoryModel::setSearchTerm(const QString &f) +{ + userSearchString_ = f.toStdString(); + + nhlog::ui()->debug("Received user query: {}", userSearchString_); + + resetDisplayedData(); +} + +bool +RoomDirectoryModel::canJoinRoom(const QString &room) const +{ + return !room.isEmpty() && cache::getRoomInfo({room.toStdString()}).empty(); +} + +std::vector +RoomDirectoryModel::getViasForRoom(const std::vector &aliases) +{ + std::vector vias; + + vias.reserve(aliases.size()); + + std::transform(aliases.begin(), aliases.end(), std::back_inserter(vias), [](const auto &alias) { + return alias.substr(alias.find(":") + 1); + }); + + // When joining a room hosted on a homeserver other than the one the + // account has been registered on, the room's server has to be explicitly + // specified in the "server_name=..." URL parameter of the Matrix Join Room + // request. For more details consult the specs: + // https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-join-roomidoralias + if (!server_.empty()) { + vias.push_back(server_); + } + + return vias; +} + +void +RoomDirectoryModel::joinRoom(const int &index) +{ + if (index >= 0 && static_cast(index) < publicRoomsData_.size()) { + const auto &chunk = publicRoomsData_[index]; + nhlog::ui()->debug("'Joining room {}", chunk.room_id); + ChatPage::instance()->joinRoomVia(chunk.room_id, getViasForRoom(chunk.aliases)); + } +} + +QVariant +RoomDirectoryModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &room_chunk = publicRoomsData_[index.row()]; + switch (role) { + case Roles::Name: + return QString::fromStdString(room_chunk.name); + case Roles::Id: + return QString::fromStdString(room_chunk.room_id); + case Roles::AvatarUrl: + return QString::fromStdString(room_chunk.avatar_url); + case Roles::Topic: + return QString::fromStdString(room_chunk.topic); + case Roles::MemberCount: + return QVariant::fromValue(room_chunk.num_joined_members); + case Roles::Previewable: + return QVariant::fromValue(room_chunk.world_readable); + case Roles::CanJoin: + return canJoinRoom(QString::fromStdString(room_chunk.room_id)); + } + } + return {}; +} + +void +RoomDirectoryModel::fetchMore(const QModelIndex &) +{ + if (!canFetchMore_) + return; + + nhlog::net()->debug("Fetching more rooms from mtxclient..."); + + mtx::requests::PublicRooms req; + req.limit = limit_; + req.since = prevBatch_; + req.filter.generic_search_term = userSearchString_; + // req.third_party_instance_id = third_party_instance_id; + auto requested_server = server_; + + reachedEndOfPagination_ = false; + emit reachedEndOfPaginationChanged(); + + loadingMoreRooms_ = true; + emit loadingMoreRoomsChanged(); + + http::client()->post_public_rooms( + req, + [requested_server, this, req](const mtx::responses::PublicRooms &res, + mtx::http::RequestErr err) { + loadingMoreRooms_ = false; + emit loadingMoreRoomsChanged(); + + if (err) { + nhlog::net()->error("Failed to retrieve rooms from mtxclient - {} - {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error, + err->parse_error); + } else if (req.filter.generic_search_term == this->userSearchString_ && + req.since == this->prevBatch_ && requested_server == this->server_) { + nhlog::net()->debug("signalling chunk to GUI thread"); + emit fetchedRoomsBatch(res.chunk, res.next_batch); + } + }, + requested_server); +} + +void +RoomDirectoryModel::displayRooms(std::vector fetched_rooms, + const std::string &next_batch) +{ + nhlog::net()->debug("Prev batch: {} | Next batch: {}", prevBatch_, next_batch); + + if (fetched_rooms.empty()) { + nhlog::net()->error("mtxclient helper thread yielded empty chunk!"); + return; + } + + beginInsertRows(QModelIndex(), + static_cast(publicRoomsData_.size()), + static_cast(publicRoomsData_.size() + fetched_rooms.size()) - 1); + this->publicRoomsData_.insert( + this->publicRoomsData_.end(), fetched_rooms.begin(), fetched_rooms.end()); + endInsertRows(); + + if (next_batch.empty()) { + canFetchMore_ = false; + reachedEndOfPagination_ = true; + emit reachedEndOfPaginationChanged(); + } + + prevBatch_ = next_batch; + + nhlog::ui()->debug("Finished loading rooms"); +} diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h new file mode 100644 index 00000000..4699474b --- /dev/null +++ b/src/RoomDirectoryModel.h @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "MatrixClient.h" +#include +#include + +#include "Logging.h" + +namespace mtx::http { +using RequestErr = const std::optional &; +} +namespace mtx::responses { +struct PublicRooms; +} + +class RoomDirectoryModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(bool loadingMoreRooms READ loadingMoreRooms NOTIFY loadingMoreRoomsChanged) + Q_PROPERTY( + bool reachedEndOfPagination READ reachedEndOfPagination NOTIFY reachedEndOfPaginationChanged) + +public: + explicit RoomDirectoryModel(QObject *parent = nullptr, const std::string &server = ""); + + enum Roles + { + Name = Qt::UserRole, + Id, + AvatarUrl, + Topic, + MemberCount, + Previewable, + CanJoin, + }; + QHash roleNames() const override; + + QVariant data(const QModelIndex &index, int role) const override; + + inline int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return static_cast(publicRoomsData_.size()); + } + + bool canFetchMore(const QModelIndex &) const override { return canFetchMore_; } + + bool loadingMoreRooms() const { return loadingMoreRooms_; } + + bool reachedEndOfPagination() const { return reachedEndOfPagination_; } + + void fetchMore(const QModelIndex &) override; + + Q_INVOKABLE void joinRoom(const int &index = -1); + +signals: + void fetchedRoomsBatch(std::vector rooms, + const std::string &next_batch); + void loadingMoreRoomsChanged(); + void reachedEndOfPaginationChanged(); + +public slots: + void setMatrixServer(const QString &s = ""); + void setSearchTerm(const QString &f); + +private slots: + + void displayRooms(std::vector rooms, + const std::string &next_batch); + +private: + bool canJoinRoom(const QString &room) const; + + static constexpr size_t limit_ = 50; + + std::string server_; + std::string userSearchString_; + std::string prevBatch_; + std::string nextBatch_; + bool canFetchMore_{true}; + bool loadingMoreRooms_{false}; + bool reachedEndOfPagination_{false}; + std::vector publicRoomsData_; + + std::vector getViasForRoom(const std::vector &room); + void resetDisplayedData(); +}; diff --git a/src/RoomsModel.cpp b/src/RoomsModel.cpp index 80f13756..8c05b7bb 100644 --- a/src/RoomsModel.cpp +++ b/src/RoomsModel.cpp @@ -14,71 +14,67 @@ RoomsModel::RoomsModel(bool showOnlyRoomWithAliases, QObject *parent) : QAbstractListModel(parent) , showOnlyRoomWithAliases_(showOnlyRoomWithAliases) { - std::vector rooms_ = cache::joinedRooms(); - roomInfos = cache::getRoomInfo(rooms_); - if (!showOnlyRoomWithAliases_) { - roomids.reserve(rooms_.size()); - roomAliases.reserve(rooms_.size()); - } + std::vector rooms_ = cache::joinedRooms(); + roomInfos = cache::getRoomInfo(rooms_); + if (!showOnlyRoomWithAliases_) { + roomids.reserve(rooms_.size()); + roomAliases.reserve(rooms_.size()); + } - for (const auto &r : rooms_) { - auto roomAliasesList = cache::client()->getRoomAliases(r); + for (const auto &r : rooms_) { + auto roomAliasesList = cache::client()->getRoomAliases(r); - if (showOnlyRoomWithAliases_) { - if (roomAliasesList && !roomAliasesList->alias.empty()) { - roomids.push_back(QString::fromStdString(r)); - roomAliases.push_back( - QString::fromStdString(roomAliasesList->alias)); - } - } else { - roomids.push_back(QString::fromStdString(r)); - roomAliases.push_back( - roomAliasesList ? QString::fromStdString(roomAliasesList->alias) : ""); - } + if (showOnlyRoomWithAliases_) { + if (roomAliasesList && !roomAliasesList->alias.empty()) { + roomids.push_back(QString::fromStdString(r)); + roomAliases.push_back(QString::fromStdString(roomAliasesList->alias)); + } + } else { + roomids.push_back(QString::fromStdString(r)); + roomAliases.push_back(roomAliasesList ? QString::fromStdString(roomAliasesList->alias) + : ""); } + } } QHash RoomsModel::roleNames() const { - return {{CompletionModel::CompletionRole, "completionRole"}, - {CompletionModel::SearchRole, "searchRole"}, - {CompletionModel::SearchRole2, "searchRole2"}, - {Roles::RoomAlias, "roomAlias"}, - {Roles::AvatarUrl, "avatarUrl"}, - {Roles::RoomID, "roomid"}, - {Roles::RoomName, "roomName"}}; + return {{CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::RoomAlias, "roomAlias"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::RoomID, "roomid"}, + {Roles::RoomName, "roomName"}}; } QVariant RoomsModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - switch (role) { - case CompletionModel::CompletionRole: { - if (UserSettings::instance()->markdown()) { - QString percentEncoding = - QUrl::toPercentEncoding(roomAliases[index.row()]); - return QString("[%1](https://matrix.to/#/%2)") - .arg(roomAliases[index.row()], percentEncoding); - } else { - return roomAliases[index.row()]; - } - } - case CompletionModel::SearchRole: - case Qt::DisplayRole: - case Roles::RoomAlias: - return roomAliases[index.row()].toHtmlEscaped(); - case CompletionModel::SearchRole2: - case Roles::RoomName: - return QString::fromStdString(roomInfos.at(roomids[index.row()]).name) - .toHtmlEscaped(); - case Roles::AvatarUrl: - return QString::fromStdString( - roomInfos.at(roomids[index.row()]).avatar_url); - case Roles::RoomID: - return roomids[index.row()]; - } + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: { + if (UserSettings::instance()->markdown()) { + QString percentEncoding = QUrl::toPercentEncoding(roomAliases[index.row()]); + return QString("[%1](https://matrix.to/#/%2)") + .arg(roomAliases[index.row()], percentEncoding); + } else { + return roomAliases[index.row()]; + } } - return {}; + case CompletionModel::SearchRole: + case Qt::DisplayRole: + case Roles::RoomAlias: + return roomAliases[index.row()].toHtmlEscaped(); + case CompletionModel::SearchRole2: + case Roles::RoomName: + return QString::fromStdString(roomInfos.at(roomids[index.row()]).name).toHtmlEscaped(); + case Roles::AvatarUrl: + return QString::fromStdString(roomInfos.at(roomids[index.row()]).avatar_url); + case Roles::RoomID: + return roomids[index.row()].toHtmlEscaped(); + } + } + return {}; } diff --git a/src/RoomsModel.h b/src/RoomsModel.h index 255f207c..b6e29974 100644 --- a/src/RoomsModel.h +++ b/src/RoomsModel.h @@ -12,26 +12,26 @@ class RoomsModel : public QAbstractListModel { public: - enum Roles - { - AvatarUrl = Qt::UserRole, - RoomAlias, - RoomID, - RoomName, - }; + enum Roles + { + AvatarUrl = Qt::UserRole, + RoomAlias, + RoomID, + RoomName, + }; - RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr); - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)roomids.size(); - } - QVariant data(const QModelIndex &index, int role) const override; + RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomids.size(); + } + QVariant data(const QModelIndex &index, int role) const override; private: - std::vector roomids; - std::vector roomAliases; - std::map roomInfos; - bool showOnlyRoomWithAliases_; + std::vector roomids; + std::vector roomAliases; + std::map roomInfos; + bool showOnlyRoomWithAliases_; }; diff --git a/src/SSOHandler.cpp b/src/SSOHandler.cpp index 8fd0828c..a6f7ba11 100644 --- a/src/SSOHandler.cpp +++ b/src/SSOHandler.cpp @@ -12,46 +12,46 @@ SSOHandler::SSOHandler(QObject *) { - QTimer::singleShot(120000, this, &SSOHandler::ssoFailed); + QTimer::singleShot(120000, this, &SSOHandler::ssoFailed); - using namespace httplib; + using namespace httplib; - svr.set_logger([](const Request &req, const Response &res) { - nhlog::net()->info("req: {}, res: {}", req.path, res.status); - }); + svr.set_logger([](const Request &req, const Response &res) { + nhlog::net()->info("req: {}, res: {}", req.path, res.status); + }); - svr.Get("/sso", [this](const Request &req, Response &res) { - if (req.has_param("loginToken")) { - auto val = req.get_param_value("loginToken"); - res.set_content("SSO success", "text/plain"); - emit ssoSuccess(val); - } else { - res.set_content("Missing loginToken for SSO login!", "text/plain"); - emit ssoFailed(); - } - }); - - std::thread t([this]() { - this->port = svr.bind_to_any_port("localhost"); - svr.listen_after_bind(); - }); - t.detach(); - - while (!svr.is_running()) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + svr.Get("/sso", [this](const Request &req, Response &res) { + if (req.has_param("loginToken")) { + auto val = req.get_param_value("loginToken"); + res.set_content("SSO success", "text/plain"); + emit ssoSuccess(val); + } else { + res.set_content("Missing loginToken for SSO login!", "text/plain"); + emit ssoFailed(); } + }); + + std::thread t([this]() { + this->port = svr.bind_to_any_port("localhost"); + svr.listen_after_bind(); + }); + t.detach(); + + while (!svr.is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } } SSOHandler::~SSOHandler() { - svr.stop(); - while (svr.is_running()) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + svr.stop(); + while (svr.is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } } std::string SSOHandler::url() const { - return "http://localhost:" + std::to_string(port) + "/sso"; + return "http://localhost:" + std::to_string(port) + "/sso"; } diff --git a/src/SSOHandler.h b/src/SSOHandler.h index bd0d424d..ab652a06 100644 --- a/src/SSOHandler.h +++ b/src/SSOHandler.h @@ -9,20 +9,20 @@ class SSOHandler : public QObject { - Q_OBJECT + Q_OBJECT public: - SSOHandler(QObject *parent = nullptr); + SSOHandler(QObject *parent = nullptr); - ~SSOHandler(); + ~SSOHandler(); - std::string url() const; + std::string url() const; signals: - void ssoSuccess(std::string token); - void ssoFailed(); + void ssoSuccess(std::string token); + void ssoFailed(); private: - httplib::Server svr; - int port = 0; + httplib::Server svr; + int port = 0; }; diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index 7bf55617..978a0480 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -24,327 +24,336 @@ SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) , old_statekey_(statekey_) , pack(std::move(pack_.pack)) { - [[maybe_unused]] static auto imageInfoType = qRegisterMetaType(); + [[maybe_unused]] static auto imageInfoType = qRegisterMetaType(); - if (!pack.pack) - pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; + if (!pack.pack) + pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; - for (const auto &e : pack.images) - shortcodes.push_back(e.first); + for (const auto &e : pack.images) + shortcodes.push_back(e.first); - connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb); + connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb); } int SingleImagePackModel::rowCount(const QModelIndex &) const { - return (int)shortcodes.size(); + return (int)shortcodes.size(); } QHash SingleImagePackModel::roleNames() const { - return { - {Roles::Url, "url"}, - {Roles::ShortCode, "shortCode"}, - {Roles::Body, "body"}, - {Roles::IsEmote, "isEmote"}, - {Roles::IsSticker, "isSticker"}, - }; + return { + {Roles::Url, "url"}, + {Roles::ShortCode, "shortCode"}, + {Roles::Body, "body"}, + {Roles::IsEmote, "isEmote"}, + {Roles::IsSticker, "isSticker"}, + }; } QVariant SingleImagePackModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - const auto &img = pack.images.at(shortcodes.at(index.row())); - switch (role) { - case Url: - return QString::fromStdString(img.url); - case ShortCode: - return QString::fromStdString(shortcodes.at(index.row())); - case Body: - return QString::fromStdString(img.body); - case IsEmote: - return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); - case IsSticker: - return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); - default: - return {}; - } + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case Url: + return QString::fromStdString(img.url); + case ShortCode: + return QString::fromStdString(shortcodes.at(index.row())); + case Body: + return QString::fromStdString(img.body); + case IsEmote: + return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + case IsSticker: + return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + default: + return {}; } - return {}; + } + return {}; } bool SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role) { - using mtx::events::msc2545::PackUsage; + using mtx::events::msc2545::PackUsage; - if (hasIndex(index.row(), index.column(), index.parent())) { - auto &img = pack.images.at(shortcodes.at(index.row())); - switch (role) { - case ShortCode: { - auto newCode = value.toString().toStdString(); + if (hasIndex(index.row(), index.column(), index.parent())) { + auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case ShortCode: { + auto newCode = value.toString().toStdString(); - // otherwise we delete this by accident - if (pack.images.count(newCode)) - return false; + // otherwise we delete this by accident + if (pack.images.count(newCode)) + return false; - auto tmp = img; - auto oldCode = shortcodes.at(index.row()); - pack.images.erase(oldCode); - shortcodes[index.row()] = newCode; - pack.images.insert({newCode, tmp}); + auto tmp = img; + auto oldCode = shortcodes.at(index.row()); + pack.images.erase(oldCode); + shortcodes[index.row()] = newCode; + pack.images.insert({newCode, tmp}); - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); - return true; - } - case Body: - img.body = value.toString().toStdString(); - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::Body}); - return true; - case IsEmote: { - bool isEmote = value.toBool(); - bool isSticker = - img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); - - img.usage.set(PackUsage::Emoji, isEmote); - img.usage.set(PackUsage::Sticker, isSticker); - - if (img.usage == pack.pack->usage) - img.usage.reset(); - - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); - - return true; - } - case IsSticker: { - bool isEmote = - img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); - bool isSticker = value.toBool(); - - img.usage.set(PackUsage::Emoji, isEmote); - img.usage.set(PackUsage::Sticker, isSticker); - - if (img.usage == pack.pack->usage) - img.usage.reset(); - - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); - - return true; - } - } + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); + return true; } - return false; + case Body: + img.body = value.toString().toStdString(); + emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::Body}); + return true; + case IsEmote: { + bool isEmote = value.toBool(); + bool isSticker = img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); + + return true; + } + case IsSticker: { + bool isEmote = img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + bool isSticker = value.toBool(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); + + return true; + } + } + } + return false; } bool SingleImagePackModel::isGloballyEnabled() const { - if (auto roomPacks = - cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { - if (auto tmp = std::get_if< - mtx::events::EphemeralEvent>( - &*roomPacks)) { - if (tmp->content.rooms.count(roomid_) && - tmp->content.rooms.at(roomid_).count(statekey_)) - return true; - } + if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = + std::get_if>( + &*roomPacks)) { + if (tmp->content.rooms.count(roomid_) && + tmp->content.rooms.at(roomid_).count(statekey_)) + return true; } - return false; + } + return false; } void SingleImagePackModel::setGloballyEnabled(bool enabled) { - mtx::events::msc2545::ImagePackRooms content{}; - if (auto roomPacks = - cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { - if (auto tmp = std::get_if< - mtx::events::EphemeralEvent>( - &*roomPacks)) { - content = tmp->content; - } + mtx::events::msc2545::ImagePackRooms content{}; + if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = + std::get_if>( + &*roomPacks)) { + content = tmp->content; } + } - if (enabled) - content.rooms[roomid_][statekey_] = {}; - else - content.rooms[roomid_].erase(statekey_); + if (enabled) + content.rooms[roomid_][statekey_] = {}; + else + content.rooms[roomid_].erase(statekey_); - http::client()->put_account_data(content, [](mtx::http::RequestErr) { - // emit this->globallyEnabledChanged(); - }); + http::client()->put_account_data(content, [](mtx::http::RequestErr) { + // emit this->globallyEnabledChanged(); + }); } bool SingleImagePackModel::canEdit() const { - if (roomid_.empty()) - return true; - else - return Permissions(QString::fromStdString(roomid_)) - .canChange(qml_mtx_events::ImagePackInRoom); + if (roomid_.empty()) + return true; + else + return Permissions(QString::fromStdString(roomid_)) + .canChange(qml_mtx_events::ImagePackInRoom); } void SingleImagePackModel::setPackname(QString val) { - auto val_ = val.toStdString(); - if (val_ != this->pack.pack->display_name) { - this->pack.pack->display_name = val_; - emit packnameChanged(); - } + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->display_name) { + this->pack.pack->display_name = val_; + emit packnameChanged(); + } } void SingleImagePackModel::setAttribution(QString val) { - auto val_ = val.toStdString(); - if (val_ != this->pack.pack->attribution) { - this->pack.pack->attribution = val_; - emit attributionChanged(); - } + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->attribution) { + this->pack.pack->attribution = val_; + emit attributionChanged(); + } } void SingleImagePackModel::setAvatarUrl(QString val) { - auto val_ = val.toStdString(); - if (val_ != this->pack.pack->avatar_url) { - this->pack.pack->avatar_url = val_; - emit avatarUrlChanged(); - } + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->avatar_url) { + this->pack.pack->avatar_url = val_; + emit avatarUrlChanged(); + } } void SingleImagePackModel::setStatekey(QString val) { - auto val_ = val.toStdString(); - if (val_ != statekey_) { - statekey_ = val_; - emit statekeyChanged(); - } + auto val_ = val.toStdString(); + if (val_ != statekey_) { + statekey_ = val_; + emit statekeyChanged(); + } } void SingleImagePackModel::setIsStickerPack(bool val) { - using mtx::events::msc2545::PackUsage; - if (val != pack.pack->is_sticker()) { - pack.pack->usage.set(PackUsage::Sticker, val); - emit isStickerPackChanged(); - } + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_sticker()) { + pack.pack->usage.set(PackUsage::Sticker, val); + emit isStickerPackChanged(); + } } void SingleImagePackModel::setIsEmotePack(bool val) { - using mtx::events::msc2545::PackUsage; - if (val != pack.pack->is_emoji()) { - pack.pack->usage.set(PackUsage::Emoji, val); - emit isEmotePackChanged(); - } + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_emoji()) { + pack.pack->usage.set(PackUsage::Emoji, val); + emit isEmotePackChanged(); + } } void SingleImagePackModel::save() { - if (roomid_.empty()) { - http::client()->put_account_data(pack, [](mtx::http::RequestErr e) { - if (e) - ChatPage::instance()->showNotification( - tr("Failed to update image pack: {}") - .arg(QString::fromStdString(e->matrix_error.error))); - }); - } else { - if (old_statekey_ != statekey_) { - http::client()->send_state_event( - roomid_, - to_string(mtx::events::EventType::ImagePackInRoom), - old_statekey_, - nlohmann::json::object(), - [](const mtx::responses::EventId &, mtx::http::RequestErr e) { - if (e) - ChatPage::instance()->showNotification( - tr("Failed to delete old image pack: {}") - .arg(QString::fromStdString(e->matrix_error.error))); - }); - } - - http::client()->send_state_event( - roomid_, - statekey_, - pack, - [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { - if (e) - ChatPage::instance()->showNotification( - tr("Failed to update image pack: {}") - .arg(QString::fromStdString(e->matrix_error.error))); - - nhlog::net()->info("Uploaded image pack: {}", statekey_); - }); + if (roomid_.empty()) { + http::client()->put_account_data(pack, [](mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } else { + if (old_statekey_ != statekey_) { + http::client()->send_state_event( + roomid_, + to_string(mtx::events::EventType::ImagePackInRoom), + old_statekey_, + nlohmann::json::object(), + [](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to delete old image pack: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + }); } + + http::client()->send_state_event( + roomid_, + statekey_, + pack, + [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + + nhlog::net()->info("Uploaded image pack: %1", statekey_); + }); + } } void SingleImagePackModel::addStickers(QList files) { - for (const auto &f : files) { - auto file = QFile(f.toLocalFile()); - if (!file.open(QFile::ReadOnly)) { - ChatPage::instance()->showNotification( - tr("Failed to open image: {}").arg(f.toLocalFile())); - return; - } - - auto bytes = file.readAll(); - auto img = utils::readImage(bytes); - - mtx::common::ImageInfo info{}; - - auto sz = img.size() / 2; - if (sz.width() > 512 || sz.height() > 512) { - sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio); - } - - info.h = sz.height(); - info.w = sz.width(); - info.size = bytes.size(); - - auto filename = f.fileName().toStdString(); - http::client()->upload( - bytes.toStdString(), - QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), - filename, - [this, filename, info](const mtx::responses::ContentURI &uri, - mtx::http::RequestErr e) { - if (e) { - ChatPage::instance()->showNotification( - tr("Failed to upload image: {}") - .arg(QString::fromStdString(e->matrix_error.error))); - return; - } - - emit addImage(uri.content_uri, filename, info); - }); + for (const auto &f : files) { + auto file = QFile(f.toLocalFile()); + if (!file.open(QFile::ReadOnly)) { + ChatPage::instance()->showNotification( + tr("Failed to open image: %1").arg(f.toLocalFile())); + return; } + + auto bytes = file.readAll(); + auto img = utils::readImage(bytes); + + mtx::common::ImageInfo info{}; + + auto sz = img.size() / 2; + if (sz.width() > 512 || sz.height() > 512) { + sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio); + } else if (img.height() < 128 && img.width() < 128) { + sz = img.size(); + } + + info.h = sz.height(); + info.w = sz.width(); + info.size = bytes.size(); + info.mimetype = QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(); + + auto filename = f.fileName().toStdString(); + http::client()->upload( + bytes.toStdString(), + QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), + filename, + [this, filename, info](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) { + if (e) { + ChatPage::instance()->showNotification( + tr("Failed to upload image: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + return; + } + + emit addImage(uri.content_uri, filename, info); + }); + } } + +void +SingleImagePackModel::remove(int idx) +{ + if (idx < (int)shortcodes.size() && idx >= 0) { + beginRemoveRows(QModelIndex(), idx, idx); + auto s = shortcodes.at(idx); + shortcodes.erase(shortcodes.begin() + idx); + pack.images.erase(s); + endRemoveRows(); + } +} + void SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info) { - mtx::events::msc2545::PackImage img{}; - img.url = uri; - img.info = info; - beginInsertRows( - QModelIndex(), static_cast(shortcodes.size()), static_cast(shortcodes.size())); + mtx::events::msc2545::PackImage img{}; + img.url = uri; + img.info = info; + beginInsertRows( + QModelIndex(), static_cast(shortcodes.size()), static_cast(shortcodes.size())); - pack.images[filename] = img; - shortcodes.push_back(filename); + pack.images[filename] = img; + shortcodes.push_back(filename); - endInsertRows(); + endInsertRows(); } diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h index cd38b3b6..cd8b0547 100644 --- a/src/SingleImagePackModel.h +++ b/src/SingleImagePackModel.h @@ -14,80 +14,78 @@ class SingleImagePackModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QString roomid READ roomid CONSTANT) - Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged) - Q_PROPERTY( - QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged) - Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged) - Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY( - bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged) - Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged) - Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY - globallyEnabledChanged) - Q_PROPERTY(bool canEdit READ canEdit CONSTANT) + Q_PROPERTY(QString roomid READ roomid CONSTANT) + Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged) + Q_PROPERTY(QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged) + Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY( + bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged) + Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged) + Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY + globallyEnabledChanged) + Q_PROPERTY(bool canEdit READ canEdit CONSTANT) public: - enum Roles - { - Url = Qt::UserRole, - ShortCode, - Body, - IsEmote, - IsSticker, - }; - Q_ENUM(Roles); + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + IsEmote, + IsSticker, + }; + Q_ENUM(Roles); - SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - bool setData(const QModelIndex &index, - const QVariant &value, - int role = Qt::EditRole) override; + SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - QString roomid() const { return QString::fromStdString(roomid_); } - QString statekey() const { return QString::fromStdString(statekey_); } - QString packname() const { return QString::fromStdString(pack.pack->display_name); } - QString attribution() const { return QString::fromStdString(pack.pack->attribution); } - QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); } - bool isStickerPack() const { return pack.pack->is_sticker(); } - bool isEmotePack() const { return pack.pack->is_emoji(); } + QString roomid() const { return QString::fromStdString(roomid_); } + QString statekey() const { return QString::fromStdString(statekey_); } + QString packname() const { return QString::fromStdString(pack.pack->display_name); } + QString attribution() const { return QString::fromStdString(pack.pack->attribution); } + QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); } + bool isStickerPack() const { return pack.pack->is_sticker(); } + bool isEmotePack() const { return pack.pack->is_emoji(); } - bool isGloballyEnabled() const; - bool canEdit() const; - void setGloballyEnabled(bool enabled); + bool isGloballyEnabled() const; + bool canEdit() const; + void setGloballyEnabled(bool enabled); - void setPackname(QString val); - void setAttribution(QString val); - void setAvatarUrl(QString val); - void setStatekey(QString val); - void setIsStickerPack(bool val); - void setIsEmotePack(bool val); + void setPackname(QString val); + void setAttribution(QString val); + void setAvatarUrl(QString val); + void setStatekey(QString val); + void setIsStickerPack(bool val); + void setIsEmotePack(bool val); - Q_INVOKABLE void save(); - Q_INVOKABLE void addStickers(QList files); + Q_INVOKABLE void save(); + Q_INVOKABLE void addStickers(QList files); + Q_INVOKABLE void remove(int index); signals: - void globallyEnabledChanged(); - void statekeyChanged(); - void attributionChanged(); - void packnameChanged(); - void avatarUrlChanged(); - void isEmotePackChanged(); - void isStickerPackChanged(); + void globallyEnabledChanged(); + void statekeyChanged(); + void attributionChanged(); + void packnameChanged(); + void avatarUrlChanged(); + void isEmotePackChanged(); + void isStickerPackChanged(); - void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info); + void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info); private slots: - void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info); + void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info); private: - std::string roomid_; - std::string statekey_, old_statekey_; + std::string roomid_; + std::string statekey_, old_statekey_; - mtx::events::msc2545::ImagePack pack; - std::vector shortcodes; + mtx::events::msc2545::ImagePack pack; + std::vector shortcodes; }; diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp index db0130c8..98a1d242 100644 --- a/src/TrayIcon.cpp +++ b/src/TrayIcon.cpp @@ -19,7 +19,7 @@ MsgCountComposedIcon::MsgCountComposedIcon(const QString &filename) : QIconEngine() { - icon_ = QIcon(filename); + icon_ = QIcon(filename); } void @@ -28,95 +28,95 @@ MsgCountComposedIcon::paint(QPainter *painter, QIcon::Mode mode, QIcon::State state) { - painter->setRenderHint(QPainter::TextAntialiasing); - painter->setRenderHint(QPainter::SmoothPixmapTransform); - painter->setRenderHint(QPainter::Antialiasing); + painter->setRenderHint(QPainter::TextAntialiasing); + painter->setRenderHint(QPainter::SmoothPixmapTransform); + painter->setRenderHint(QPainter::Antialiasing); - icon_.paint(painter, rect, Qt::AlignCenter, mode, state); + icon_.paint(painter, rect, Qt::AlignCenter, mode, state); - if (msgCount <= 0) - return; + if (msgCount <= 0) + return; - QColor backgroundColor("red"); - QColor textColor("white"); + QColor backgroundColor("red"); + QColor textColor("white"); - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(backgroundColor); + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor); - QFont f; - f.setPointSizeF(8); - f.setWeight(QFont::Thin); + QFont f; + f.setPointSizeF(8); + f.setWeight(QFont::Thin); - painter->setBrush(brush); - painter->setPen(Qt::NoPen); - painter->setFont(f); + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + painter->setFont(f); - QRectF bubble(rect.width() - BubbleDiameter, - rect.height() - BubbleDiameter, - BubbleDiameter, - BubbleDiameter); - painter->drawEllipse(bubble); - painter->setPen(QPen(textColor)); - painter->setBrush(Qt::NoBrush); - painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); + QRectF bubble(rect.width() - BubbleDiameter, + rect.height() - BubbleDiameter, + BubbleDiameter, + BubbleDiameter); + painter->drawEllipse(bubble); + painter->setPen(QPen(textColor)); + painter->setBrush(Qt::NoBrush); + painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); } QIconEngine * MsgCountComposedIcon::clone() const { - return new MsgCountComposedIcon(*this); + return new MsgCountComposedIcon(*this); } QList MsgCountComposedIcon::availableSizes(QIcon::Mode mode, QIcon::State state) const { - Q_UNUSED(mode); - Q_UNUSED(state); - QList sizes; - sizes.append(QSize(24, 24)); - sizes.append(QSize(32, 32)); - sizes.append(QSize(48, 48)); - sizes.append(QSize(64, 64)); - sizes.append(QSize(128, 128)); - sizes.append(QSize(256, 256)); - return sizes; + Q_UNUSED(mode); + Q_UNUSED(state); + QList sizes; + sizes.append(QSize(24, 24)); + sizes.append(QSize(32, 32)); + sizes.append(QSize(48, 48)); + sizes.append(QSize(64, 64)); + sizes.append(QSize(128, 128)); + sizes.append(QSize(256, 256)); + return sizes; } QPixmap MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) { - QImage img(size, QImage::Format_ARGB32); - img.fill(qRgba(0, 0, 0, 0)); - QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); - { - QPainter painter(&result); - paint(&painter, QRect(QPoint(0, 0), size), mode, state); - } - return result; + QImage img(size, QImage::Format_ARGB32); + img.fill(qRgba(0, 0, 0, 0)); + QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); + { + QPainter painter(&result); + paint(&painter, QRect(QPoint(0, 0), size), mode, state); + } + return result; } TrayIcon::TrayIcon(const QString &filename, QWidget *parent) : QSystemTrayIcon(parent) { #if defined(Q_OS_MAC) || defined(Q_OS_WIN) - setIcon(QIcon(filename)); + setIcon(QIcon(filename)); #else - icon_ = new MsgCountComposedIcon(filename); - setIcon(QIcon(icon_)); + icon_ = new MsgCountComposedIcon(filename); + setIcon(QIcon(icon_)); #endif - QMenu *menu = new QMenu(parent); - setContextMenu(menu); + QMenu *menu = new QMenu(parent); + setContextMenu(menu); - viewAction_ = new QAction(tr("Show"), this); - quitAction_ = new QAction(tr("Quit"), this); + viewAction_ = new QAction(tr("Show"), this); + quitAction_ = new QAction(tr("Quit"), this); - connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show())); - connect(quitAction_, &QAction::triggered, this, QApplication::quit); + connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show())); + connect(quitAction_, &QAction::triggered, this, QApplication::quit); - menu->addAction(viewAction_); - menu->addAction(quitAction_); + menu->addAction(viewAction_); + menu->addAction(quitAction_); } void @@ -127,25 +127,25 @@ TrayIcon::setUnreadCount(int count) // currently, to avoid writing obj-c code, ignore deprecated warnings on the badge functions #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - auto labelText = count == 0 ? "" : QString::number(count); + auto labelText = count == 0 ? "" : QString::number(count); - if (labelText == QtMac::badgeLabelText()) - return; + if (labelText == QtMac::badgeLabelText()) + return; - QtMac::setBadgeLabelText(labelText); + QtMac::setBadgeLabelText(labelText); #pragma clang diagnostic pop #elif defined(Q_OS_WIN) // FIXME: Find a way to use Windows apis for the badge counter (if any). #else - if (count == icon_->msgCount) - return; + if (count == icon_->msgCount) + return; - // Custom drawing on Linux. - MsgCountComposedIcon *tmp = static_cast(icon_->clone()); - tmp->msgCount = count; + // Custom drawing on Linux. + MsgCountComposedIcon *tmp = static_cast(icon_->clone()); + tmp->msgCount = count; - setIcon(QIcon(tmp)); + setIcon(QIcon(tmp)); - icon_ = tmp; + icon_ = tmp; #endif } diff --git a/src/TrayIcon.h b/src/TrayIcon.h index 10dfafc5..1ce7fb0b 100644 --- a/src/TrayIcon.h +++ b/src/TrayIcon.h @@ -16,33 +16,33 @@ class QPainter; class MsgCountComposedIcon : public QIconEngine { public: - MsgCountComposedIcon(const QString &filename); + MsgCountComposedIcon(const QString &filename); - void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; - QIconEngine *clone() const override; - QList availableSizes(QIcon::Mode mode, QIcon::State state) const override; - QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + QIconEngine *clone() const override; + QList availableSizes(QIcon::Mode mode, QIcon::State state) const override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; - int msgCount = 0; + int msgCount = 0; private: - const int BubbleDiameter = 17; + const int BubbleDiameter = 17; - QIcon icon_; + QIcon icon_; }; class TrayIcon : public QSystemTrayIcon { - Q_OBJECT + Q_OBJECT public: - TrayIcon(const QString &filename, QWidget *parent); + TrayIcon(const QString &filename, QWidget *parent); public slots: - void setUnreadCount(int count); + void setUnreadCount(int count); private: - QAction *viewAction_; - QAction *quitAction_; + QAction *viewAction_; + QAction *quitAction_; - MsgCountComposedIcon *icon_; + MsgCountComposedIcon *icon_; }; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index f67c5e2d..340709a6 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -26,14 +25,14 @@ #include #include "Cache.h" -#include "CallDevices.h" #include "Config.h" #include "MatrixClient.h" -#include "Olm.h" #include "UserSettingsPage.h" #include "Utils.h" +#include "encryption/Olm.h" #include "ui/FlatButton.h" #include "ui/ToggleButton.h" +#include "voip/CallDevices.h" #include "config/nheko.h" @@ -41,1420 +40,1484 @@ QSharedPointer UserSettings::instance_; UserSettings::UserSettings() { - connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { - instance_.clear(); - }); + connect( + QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); }); } QSharedPointer UserSettings::instance() { - return instance_; + return instance_; } void UserSettings::initialize(std::optional profile) { - instance_.reset(new UserSettings()); - instance_->load(profile); + instance_.reset(new UserSettings()); + instance_->load(profile); } void UserSettings::load(std::optional profile) { - tray_ = settings.value("user/window/tray", false).toBool(); - startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); + tray_ = settings.value("user/window/tray", false).toBool(); + startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); - roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt(); - communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt(); + roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt(); + communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt(); - hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); - hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool(); - groupView_ = settings.value("user/group_view", true).toBool(); - hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); - buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); - timelineMaxWidth_ = settings.value("user/timeline/max_width", 0).toInt(); - messageHoverHighlight_ = - settings.value("user/timeline/message_hover_highlight", false).toBool(); - enlargeEmojiOnlyMessages_ = - settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool(); - markdown_ = settings.value("user/markdown_enabled", true).toBool(); - typingNotifications_ = settings.value("user/typing_notifications", true).toBool(); - sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); - readReceipts_ = settings.value("user/read_receipts", true).toBool(); - theme_ = settings.value("user/theme", defaultTheme_).toString(); - font_ = settings.value("user/font_family", "default").toString(); - avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); - decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); - privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); - privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); - mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); - baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); - auto tempPresence = settings.value("user/presence", "").toString().toStdString(); - auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); - if (presenceValue < 0) - presenceValue = 0; - presence_ = static_cast(presenceValue); - ringtone_ = settings.value("user/ringtone", "Default").toString(); - microphone_ = settings.value("user/microphone", QString()).toString(); - camera_ = settings.value("user/camera", QString()).toString(); - cameraResolution_ = settings.value("user/camera_resolution", QString()).toString(); - cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString(); - screenShareFrameRate_ = settings.value("user/screen_share_frame_rate", 5).toInt(); - screenSharePiP_ = settings.value("user/screen_share_pip", true).toBool(); - screenShareRemoteVideo_ = settings.value("user/screen_share_remote_video", false).toBool(); - screenShareHideCursor_ = settings.value("user/screen_share_hide_cursor", false).toBool(); - useStunServer_ = settings.value("user/use_stun_server", false).toBool(); + hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); + hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool(); + groupView_ = settings.value("user/group_view", true).toBool(); + hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); + buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); + timelineMaxWidth_ = settings.value("user/timeline/max_width", 0).toInt(); + messageHoverHighlight_ = + settings.value("user/timeline/message_hover_highlight", false).toBool(); + enlargeEmojiOnlyMessages_ = + settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool(); + markdown_ = settings.value("user/markdown_enabled", true).toBool(); + animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool(); + typingNotifications_ = settings.value("user/typing_notifications", true).toBool(); + sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); + readReceipts_ = settings.value("user/read_receipts", true).toBool(); + theme_ = settings.value("user/theme", defaultTheme_).toString(); + font_ = settings.value("user/font_family", "default").toString(); + avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); + useIdenticon_ = settings.value("user/use_identicon", true).toBool(); + decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); + privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); + privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); + mobileMode_ = settings.value("user/mobile_mode", false).toBool(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); + baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); + auto tempPresence = settings.value("user/presence", "").toString().toStdString(); + auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); + if (presenceValue < 0) + presenceValue = 0; + presence_ = static_cast(presenceValue); + ringtone_ = settings.value("user/ringtone", "Default").toString(); + microphone_ = settings.value("user/microphone", QString()).toString(); + camera_ = settings.value("user/camera", QString()).toString(); + cameraResolution_ = settings.value("user/camera_resolution", QString()).toString(); + cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString(); + screenShareFrameRate_ = settings.value("user/screen_share_frame_rate", 5).toInt(); + screenSharePiP_ = settings.value("user/screen_share_pip", true).toBool(); + screenShareRemoteVideo_ = settings.value("user/screen_share_remote_video", false).toBool(); + screenShareHideCursor_ = settings.value("user/screen_share_hide_cursor", false).toBool(); + useStunServer_ = settings.value("user/use_stun_server", false).toBool(); - if (profile) // set to "" if it's the default to maintain compatibility - profile_ = (*profile == "default") ? "" : *profile; - else - profile_ = settings.value("user/currentProfile", "").toString(); + if (profile) // set to "" if it's the default to maintain compatibility + profile_ = (*profile == "default") ? "" : *profile; + else + profile_ = settings.value("user/currentProfile", "").toString(); - QString prefix = - (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; - accessToken_ = settings.value(prefix + "auth/access_token", "").toString(); - homeserver_ = settings.value(prefix + "auth/home_server", "").toString(); - userId_ = settings.value(prefix + "auth/user_id", "").toString(); - deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); + QString prefix = (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; + accessToken_ = settings.value(prefix + "auth/access_token", "").toString(); + homeserver_ = settings.value(prefix + "auth/home_server", "").toString(); + userId_ = settings.value(prefix + "auth/user_id", "").toString(); + deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); - shareKeysWithTrustedUsers_ = - settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false) - .toBool(); - onlyShareKeysWithVerifiedUsers_ = - settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); + shareKeysWithTrustedUsers_ = + settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false).toBool(); + onlyShareKeysWithVerifiedUsers_ = + settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); + useOnlineKeyBackup_ = settings.value(prefix + "user/online_key_backup", false).toBool(); - disableCertificateValidation_ = - settings.value("disable_certificate_validation", false).toBool(); + disableCertificateValidation_ = + settings.value("disable_certificate_validation", false).toBool(); - applyTheme(); + applyTheme(); } void UserSettings::setMessageHoverHighlight(bool state) { - if (state == messageHoverHighlight_) - return; - messageHoverHighlight_ = state; - emit messageHoverHighlightChanged(state); - save(); + if (state == messageHoverHighlight_) + return; + messageHoverHighlight_ = state; + emit messageHoverHighlightChanged(state); + save(); } void UserSettings::setEnlargeEmojiOnlyMessages(bool state) { - if (state == enlargeEmojiOnlyMessages_) - return; - enlargeEmojiOnlyMessages_ = state; - emit enlargeEmojiOnlyMessagesChanged(state); - save(); + if (state == enlargeEmojiOnlyMessages_) + return; + enlargeEmojiOnlyMessages_ = state; + emit enlargeEmojiOnlyMessagesChanged(state); + save(); } void UserSettings::setTray(bool state) { - if (state == tray_) - return; - tray_ = state; - emit trayChanged(state); - save(); + if (state == tray_) + return; + tray_ = state; + emit trayChanged(state); + save(); } void UserSettings::setStartInTray(bool state) { - if (state == startInTray_) - return; - startInTray_ = state; - emit startInTrayChanged(state); - save(); + if (state == startInTray_) + return; + startInTray_ = state; + emit startInTrayChanged(state); + save(); } void UserSettings::setMobileMode(bool state) { - if (state == mobileMode_) - return; - mobileMode_ = state; - emit mobileModeChanged(state); - save(); + if (state == mobileMode_) + return; + mobileMode_ = state; + emit mobileModeChanged(state); + save(); } void UserSettings::setGroupView(bool state) { - if (groupView_ == state) - return; + if (groupView_ == state) + return; - groupView_ = state; - emit groupViewStateChanged(state); - save(); + groupView_ = state; + emit groupViewStateChanged(state); + save(); } void UserSettings::setHiddenTags(QStringList hiddenTags) { - hiddenTags_ = hiddenTags; - save(); + hiddenTags_ = hiddenTags; + save(); } void UserSettings::setMarkdown(bool state) { - if (state == markdown_) - return; - markdown_ = state; - emit markdownChanged(state); - save(); + if (state == markdown_) + return; + markdown_ = state; + emit markdownChanged(state); + save(); +} + +void +UserSettings::setAnimateImagesOnHover(bool state) +{ + if (state == animateImagesOnHover_) + return; + animateImagesOnHover_ = state; + emit animateImagesOnHoverChanged(state); + save(); } void UserSettings::setReadReceipts(bool state) { - if (state == readReceipts_) - return; - readReceipts_ = state; - emit readReceiptsChanged(state); - save(); + if (state == readReceipts_) + return; + readReceipts_ = state; + emit readReceiptsChanged(state); + save(); } void UserSettings::setTypingNotifications(bool state) { - if (state == typingNotifications_) - return; - typingNotifications_ = state; - emit typingNotificationsChanged(state); - save(); + if (state == typingNotifications_) + return; + typingNotifications_ = state; + emit typingNotificationsChanged(state); + save(); } void UserSettings::setSortByImportance(bool state) { - if (state == sortByImportance_) - return; - sortByImportance_ = state; - emit roomSortingChanged(state); - save(); + if (state == sortByImportance_) + return; + sortByImportance_ = state; + emit roomSortingChanged(state); + save(); } void UserSettings::setButtonsInTimeline(bool state) { - if (state == buttonsInTimeline_) - return; - buttonsInTimeline_ = state; - emit buttonInTimelineChanged(state); - save(); + if (state == buttonsInTimeline_) + return; + buttonsInTimeline_ = state; + emit buttonInTimelineChanged(state); + save(); } void UserSettings::setTimelineMaxWidth(int state) { - if (state == timelineMaxWidth_) - return; - timelineMaxWidth_ = state; - emit timelineMaxWidthChanged(state); - save(); + if (state == timelineMaxWidth_) + return; + timelineMaxWidth_ = state; + emit timelineMaxWidthChanged(state); + save(); } void UserSettings::setCommunityListWidth(int state) { - if (state == communityListWidth_) - return; - communityListWidth_ = state; - emit communityListWidthChanged(state); - save(); + if (state == communityListWidth_) + return; + communityListWidth_ = state; + emit communityListWidthChanged(state); + save(); } void UserSettings::setRoomListWidth(int state) { - if (state == roomListWidth_) - return; - roomListWidth_ = state; - emit roomListWidthChanged(state); - save(); + if (state == roomListWidth_) + return; + roomListWidth_ = state; + emit roomListWidthChanged(state); + save(); } void UserSettings::setDesktopNotifications(bool state) { - if (state == hasDesktopNotifications_) - return; - hasDesktopNotifications_ = state; - emit desktopNotificationsChanged(state); - save(); + if (state == hasDesktopNotifications_) + return; + hasDesktopNotifications_ = state; + emit desktopNotificationsChanged(state); + save(); } void UserSettings::setAlertOnNotification(bool state) { - if (state == hasAlertOnNotification_) - return; - hasAlertOnNotification_ = state; - emit alertOnNotificationChanged(state); - save(); + if (state == hasAlertOnNotification_) + return; + hasAlertOnNotification_ = state; + emit alertOnNotificationChanged(state); + save(); } void UserSettings::setAvatarCircles(bool state) { - if (state == avatarCircles_) - return; - avatarCircles_ = state; - emit avatarCirclesChanged(state); - save(); + if (state == avatarCircles_) + return; + avatarCircles_ = state; + emit avatarCirclesChanged(state); + save(); } void UserSettings::setDecryptSidebar(bool state) { - if (state == decryptSidebar_) - return; - decryptSidebar_ = state; - emit decryptSidebarChanged(state); - save(); + if (state == decryptSidebar_) + return; + decryptSidebar_ = state; + emit decryptSidebarChanged(state); + save(); } void UserSettings::setPrivacyScreen(bool state) { - if (state == privacyScreen_) { - return; - } - privacyScreen_ = state; - emit privacyScreenChanged(state); - save(); + if (state == privacyScreen_) { + return; + } + privacyScreen_ = state; + emit privacyScreenChanged(state); + save(); } void UserSettings::setPrivacyScreenTimeout(int state) { - if (state == privacyScreenTimeout_) { - return; - } - privacyScreenTimeout_ = state; - emit privacyScreenTimeoutChanged(state); - save(); + if (state == privacyScreenTimeout_) { + return; + } + privacyScreenTimeout_ = state; + emit privacyScreenTimeoutChanged(state); + save(); } void UserSettings::setFontSize(double size) { - if (size == baseFontSize_) - return; - baseFontSize_ = size; - emit fontSizeChanged(size); - save(); + if (size == baseFontSize_) + return; + baseFontSize_ = size; + emit fontSizeChanged(size); + save(); } void UserSettings::setFontFamily(QString family) { - if (family == font_) - return; - font_ = family; - emit fontChanged(family); - save(); + if (family == font_) + return; + font_ = family; + emit fontChanged(family); + save(); } void UserSettings::setEmojiFontFamily(QString family) { - if (family == emojiFont_) - return; + if (family == emojiFont_) + return; - if (family == tr("Default")) { - emojiFont_ = "default"; - } else { - emojiFont_ = family; - } + if (family == tr("Default")) { + emojiFont_ = "default"; + } else { + emojiFont_ = family; + } - emit emojiFontChanged(family); - save(); + emit emojiFontChanged(family); + save(); } void UserSettings::setPresence(Presence state) { - if (state == presence_) - return; - presence_ = state; - emit presenceChanged(state); - save(); + if (state == presence_) + return; + presence_ = state; + emit presenceChanged(state); + save(); } void UserSettings::setTheme(QString theme) { - if (theme == theme_) - return; - theme_ = theme; - save(); - applyTheme(); - emit themeChanged(theme); + if (theme == theme_) + return; + theme_ = theme; + save(); + applyTheme(); + emit themeChanged(theme); } void UserSettings::setUseStunServer(bool useStunServer) { - if (useStunServer == useStunServer_) - return; - useStunServer_ = useStunServer; - emit useStunServerChanged(useStunServer); - save(); + if (useStunServer == useStunServer_) + return; + useStunServer_ = useStunServer; + emit useStunServerChanged(useStunServer); + save(); } void UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys) { - if (shareKeys == onlyShareKeysWithVerifiedUsers_) - return; + if (shareKeys == onlyShareKeysWithVerifiedUsers_) + return; - onlyShareKeysWithVerifiedUsers_ = shareKeys; - emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); - save(); + onlyShareKeysWithVerifiedUsers_ = shareKeys; + emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); + save(); } void UserSettings::setShareKeysWithTrustedUsers(bool shareKeys) { - if (shareKeys == shareKeysWithTrustedUsers_) - return; + if (shareKeys == shareKeysWithTrustedUsers_) + return; - shareKeysWithTrustedUsers_ = shareKeys; - emit shareKeysWithTrustedUsersChanged(shareKeys); - save(); + shareKeysWithTrustedUsers_ = shareKeys; + emit shareKeysWithTrustedUsersChanged(shareKeys); + save(); +} + +void +UserSettings::setUseOnlineKeyBackup(bool useBackup) +{ + if (useBackup == useOnlineKeyBackup_) + return; + + useOnlineKeyBackup_ = useBackup; + emit useOnlineKeyBackupChanged(useBackup); + save(); } void UserSettings::setRingtone(QString ringtone) { - if (ringtone == ringtone_) - return; - ringtone_ = ringtone; - emit ringtoneChanged(ringtone); - save(); + if (ringtone == ringtone_) + return; + ringtone_ = ringtone; + emit ringtoneChanged(ringtone); + save(); } void UserSettings::setMicrophone(QString microphone) { - if (microphone == microphone_) - return; - microphone_ = microphone; - emit microphoneChanged(microphone); - save(); + if (microphone == microphone_) + return; + microphone_ = microphone; + emit microphoneChanged(microphone); + save(); } void UserSettings::setCamera(QString camera) { - if (camera == camera_) - return; - camera_ = camera; - emit cameraChanged(camera); - save(); + if (camera == camera_) + return; + camera_ = camera; + emit cameraChanged(camera); + save(); } void UserSettings::setCameraResolution(QString resolution) { - if (resolution == cameraResolution_) - return; - cameraResolution_ = resolution; - emit cameraResolutionChanged(resolution); - save(); + if (resolution == cameraResolution_) + return; + cameraResolution_ = resolution; + emit cameraResolutionChanged(resolution); + save(); } void UserSettings::setCameraFrameRate(QString frameRate) { - if (frameRate == cameraFrameRate_) - return; - cameraFrameRate_ = frameRate; - emit cameraFrameRateChanged(frameRate); - save(); + if (frameRate == cameraFrameRate_) + return; + cameraFrameRate_ = frameRate; + emit cameraFrameRateChanged(frameRate); + save(); } void UserSettings::setScreenShareFrameRate(int frameRate) { - if (frameRate == screenShareFrameRate_) - return; - screenShareFrameRate_ = frameRate; - emit screenShareFrameRateChanged(frameRate); - save(); + if (frameRate == screenShareFrameRate_) + return; + screenShareFrameRate_ = frameRate; + emit screenShareFrameRateChanged(frameRate); + save(); } void UserSettings::setScreenSharePiP(bool state) { - if (state == screenSharePiP_) - return; - screenSharePiP_ = state; - emit screenSharePiPChanged(state); - save(); + if (state == screenSharePiP_) + return; + screenSharePiP_ = state; + emit screenSharePiPChanged(state); + save(); } void UserSettings::setScreenShareRemoteVideo(bool state) { - if (state == screenShareRemoteVideo_) - return; - screenShareRemoteVideo_ = state; - emit screenShareRemoteVideoChanged(state); - save(); + if (state == screenShareRemoteVideo_) + return; + screenShareRemoteVideo_ = state; + emit screenShareRemoteVideoChanged(state); + save(); } void UserSettings::setScreenShareHideCursor(bool state) { - if (state == screenShareHideCursor_) - return; - screenShareHideCursor_ = state; - emit screenShareHideCursorChanged(state); - save(); + if (state == screenShareHideCursor_) + return; + screenShareHideCursor_ = state; + emit screenShareHideCursorChanged(state); + save(); } void UserSettings::setProfile(QString profile) { - if (profile == profile_) - return; - profile_ = profile; - emit profileChanged(profile_); - save(); + if (profile == profile_) + return; + profile_ = profile; + emit profileChanged(profile_); + save(); } void UserSettings::setUserId(QString userId) { - if (userId == userId_) - return; - userId_ = userId; - emit userIdChanged(userId_); - save(); + if (userId == userId_) + return; + userId_ = userId; + emit userIdChanged(userId_); + save(); } void UserSettings::setAccessToken(QString accessToken) { - if (accessToken == accessToken_) - return; - accessToken_ = accessToken; - emit accessTokenChanged(accessToken_); - save(); + if (accessToken == accessToken_) + return; + accessToken_ = accessToken; + emit accessTokenChanged(accessToken_); + save(); } void UserSettings::setDeviceId(QString deviceId) { - if (deviceId == deviceId_) - return; - deviceId_ = deviceId; - emit deviceIdChanged(deviceId_); - save(); + if (deviceId == deviceId_) + return; + deviceId_ = deviceId; + emit deviceIdChanged(deviceId_); + save(); } void UserSettings::setHomeserver(QString homeserver) { - if (homeserver == homeserver_) - return; - homeserver_ = homeserver; - emit homeserverChanged(homeserver_); - save(); + if (homeserver == homeserver_) + return; + homeserver_ = homeserver; + emit homeserverChanged(homeserver_); + save(); } void UserSettings::setDisableCertificateValidation(bool disabled) { - if (disabled == disableCertificateValidation_) - return; - disableCertificateValidation_ = disabled; - http::client()->verify_certificates(!disabled); - emit disableCertificateValidationChanged(disabled); - save(); + if (disabled == disableCertificateValidation_) + return; + disableCertificateValidation_ = disabled; + http::client()->verify_certificates(!disabled); + emit disableCertificateValidationChanged(disabled); +} + +void +UserSettings::setUseIdenticon(bool state) +{ + if (state == useIdenticon_) + return; + useIdenticon_ = state; + emit useIdenticonChanged(useIdenticon_); + save(); } void UserSettings::applyTheme() { - QFile stylefile; + QFile stylefile; - if (this->theme() == "light") { - stylefile.setFileName(":/styles/styles/nheko.qss"); - } else if (this->theme() == "dark") { - stylefile.setFileName(":/styles/styles/nheko-dark.qss"); - } else { - stylefile.setFileName(":/styles/styles/system.qss"); - } - QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString())); + if (this->theme() == "light") { + stylefile.setFileName(":/styles/styles/nheko.qss"); + } else if (this->theme() == "dark") { + stylefile.setFileName(":/styles/styles/nheko-dark.qss"); + } else { + stylefile.setFileName(":/styles/styles/system.qss"); + } + QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString())); - stylefile.open(QFile::ReadOnly); - QString stylesheet = QString(stylefile.readAll()); + stylefile.open(QFile::ReadOnly); + QString stylesheet = QString(stylefile.readAll()); - qobject_cast(QApplication::instance())->setStyleSheet(stylesheet); + qobject_cast(QApplication::instance())->setStyleSheet(stylesheet); } void UserSettings::save() { - settings.beginGroup("user"); + settings.beginGroup("user"); - settings.beginGroup("window"); - settings.setValue("tray", tray_); - settings.setValue("start_in_tray", startInTray_); - settings.endGroup(); // window + settings.beginGroup("window"); + settings.setValue("tray", tray_); + settings.setValue("start_in_tray", startInTray_); + settings.endGroup(); // window - settings.beginGroup("sidebar"); - settings.setValue("community_list_width", communityListWidth_); - settings.setValue("room_list_width", roomListWidth_); - settings.endGroup(); // window + settings.beginGroup("sidebar"); + settings.setValue("community_list_width", communityListWidth_); + settings.setValue("room_list_width", roomListWidth_); + settings.endGroup(); // window - settings.beginGroup("timeline"); - settings.setValue("buttons", buttonsInTimeline_); - settings.setValue("message_hover_highlight", messageHoverHighlight_); - settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_); - settings.setValue("max_width", timelineMaxWidth_); - settings.endGroup(); // timeline + settings.beginGroup("timeline"); + settings.setValue("buttons", buttonsInTimeline_); + settings.setValue("message_hover_highlight", messageHoverHighlight_); + settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_); + settings.setValue("max_width", timelineMaxWidth_); + settings.endGroup(); // timeline - settings.setValue("avatar_circles", avatarCircles_); - settings.setValue("decrypt_sidebar", decryptSidebar_); - settings.setValue("privacy_screen", privacyScreen_); - settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("mobile_mode", mobileMode_); - settings.setValue("font_size", baseFontSize_); - settings.setValue("typing_notifications", typingNotifications_); - settings.setValue("sort_by_unread", sortByImportance_); - settings.setValue("minor_events", sortByImportance_); - settings.setValue("read_receipts", readReceipts_); - settings.setValue("group_view", groupView_); - settings.setValue("hidden_tags", hiddenTags_); - settings.setValue("markdown_enabled", markdown_); - settings.setValue("desktop_notifications", hasDesktopNotifications_); - settings.setValue("alert_on_notification", hasAlertOnNotification_); - settings.setValue("theme", theme()); - settings.setValue("font_family", font_); - settings.setValue("emoji_font_family", emojiFont_); - settings.setValue("presence", - QString::fromUtf8(QMetaEnum::fromType().valueToKey( - static_cast(presence_)))); - settings.setValue("ringtone", ringtone_); - settings.setValue("microphone", microphone_); - settings.setValue("camera", camera_); - settings.setValue("camera_resolution", cameraResolution_); - settings.setValue("camera_frame_rate", cameraFrameRate_); - settings.setValue("screen_share_frame_rate", screenShareFrameRate_); - settings.setValue("screen_share_pip", screenSharePiP_); - settings.setValue("screen_share_remote_video", screenShareRemoteVideo_); - settings.setValue("screen_share_hide_cursor", screenShareHideCursor_); - settings.setValue("use_stun_server", useStunServer_); - settings.setValue("currentProfile", profile_); + settings.setValue("avatar_circles", avatarCircles_); + settings.setValue("decrypt_sidebar", decryptSidebar_); + settings.setValue("privacy_screen", privacyScreen_); + settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); + settings.setValue("mobile_mode", mobileMode_); + settings.setValue("font_size", baseFontSize_); + settings.setValue("typing_notifications", typingNotifications_); + settings.setValue("sort_by_unread", sortByImportance_); + settings.setValue("minor_events", sortByImportance_); + settings.setValue("read_receipts", readReceipts_); + settings.setValue("group_view", groupView_); + settings.setValue("hidden_tags", hiddenTags_); + settings.setValue("markdown_enabled", markdown_); + settings.setValue("animate_images_on_hover", animateImagesOnHover_); + settings.setValue("desktop_notifications", hasDesktopNotifications_); + settings.setValue("alert_on_notification", hasAlertOnNotification_); + settings.setValue("theme", theme()); + settings.setValue("font_family", font_); + settings.setValue("emoji_font_family", emojiFont_); + settings.setValue( + "presence", + QString::fromUtf8(QMetaEnum::fromType().valueToKey(static_cast(presence_)))); + settings.setValue("ringtone", ringtone_); + settings.setValue("microphone", microphone_); + settings.setValue("camera", camera_); + settings.setValue("camera_resolution", cameraResolution_); + settings.setValue("camera_frame_rate", cameraFrameRate_); + settings.setValue("screen_share_frame_rate", screenShareFrameRate_); + settings.setValue("screen_share_pip", screenSharePiP_); + settings.setValue("screen_share_remote_video", screenShareRemoteVideo_); + settings.setValue("screen_share_hide_cursor", screenShareHideCursor_); + settings.setValue("use_stun_server", useStunServer_); + settings.setValue("currentProfile", profile_); + settings.setValue("use_identicon", useIdenticon_); - settings.endGroup(); // user + settings.endGroup(); // user - QString prefix = - (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; - settings.setValue(prefix + "auth/access_token", accessToken_); - settings.setValue(prefix + "auth/home_server", homeserver_); - settings.setValue(prefix + "auth/user_id", userId_); - settings.setValue(prefix + "auth/device_id", deviceId_); + QString prefix = (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; + settings.setValue(prefix + "auth/access_token", accessToken_); + settings.setValue(prefix + "auth/home_server", homeserver_); + settings.setValue(prefix + "auth/user_id", userId_); + settings.setValue(prefix + "auth/device_id", deviceId_); - settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", - shareKeysWithTrustedUsers_); - settings.setValue(prefix + "user/only_share_keys_with_verified_users", - onlyShareKeysWithVerifiedUsers_); + settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); + settings.setValue(prefix + "user/only_share_keys_with_verified_users", + onlyShareKeysWithVerifiedUsers_); + settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_); - settings.setValue("disable_certificate_validation", disableCertificateValidation_); + settings.setValue("disable_certificate_validation", disableCertificateValidation_); - settings.sync(); + settings.sync(); } HorizontalLine::HorizontalLine(QWidget *parent) : QFrame{parent} { - setFrameShape(QFrame::HLine); - setFrameShadow(QFrame::Sunken); + setFrameShape(QFrame::HLine); + setFrameShadow(QFrame::Sunken); } UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidget *parent) : QWidget{parent} , settings_{settings} { - topLayout_ = new QVBoxLayout{this}; + topLayout_ = new QVBoxLayout{this}; - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - auto backBtn_ = new FlatButton{this}; - backBtn_->setMinimumSize(QSize(24, 24)); - backBtn_->setIcon(icon); - backBtn_->setIconSize(QSize(24, 24)); + auto backBtn_ = new FlatButton{this}; + backBtn_->setMinimumSize(QSize(24, 24)); + backBtn_->setIcon(icon); + backBtn_->setIconSize(QSize(24, 24)); - QFont font; - font.setPointSizeF(font.pointSizeF() * 1.1); + QFont font; + font.setPointSizeF(font.pointSizeF() * 1.1); - auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os)); - if (QCoreApplication::applicationName() != "nheko") - versionInfo->setText(versionInfo->text() + " | " + - tr("profile: %1").arg(QCoreApplication::applicationName())); - versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); + auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os)); + if (QCoreApplication::applicationName() != "nheko") + versionInfo->setText(versionInfo->text() + " | " + + tr("profile: %1").arg(QCoreApplication::applicationName())); + versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); - topBarLayout_ = new QHBoxLayout; - topBarLayout_->setSpacing(0); - topBarLayout_->setMargin(0); - topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); - topBarLayout_->addStretch(1); + topBarLayout_ = new QHBoxLayout; + topBarLayout_->setSpacing(0); + topBarLayout_->setMargin(0); + topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); + topBarLayout_->addStretch(1); - formLayout_ = new QFormLayout; + formLayout_ = new QFormLayout; - formLayout_->setLabelAlignment(Qt::AlignLeft); - formLayout_->setFormAlignment(Qt::AlignRight); - formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows); - formLayout_->setHorizontalSpacing(0); + formLayout_->setLabelAlignment(Qt::AlignLeft); + formLayout_->setFormAlignment(Qt::AlignRight); + formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows); + formLayout_->setHorizontalSpacing(0); - auto general_ = new QLabel{tr("GENERAL"), this}; - general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - general_->setFont(font); + auto general_ = new QLabel{tr("GENERAL"), this}; + general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + general_->setFont(font); - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - decryptSidebar_ = new Toggle(this); - privacyScreen_ = new Toggle{this}; - onlyShareKeysWithVerifiedUsers_ = new Toggle(this); - shareKeysWithTrustedUsers_ = new Toggle(this); - groupViewToggle_ = new Toggle{this}; - timelineButtonsToggle_ = new Toggle{this}; - typingNotifications_ = new Toggle{this}; - messageHoverHighlight_ = new Toggle{this}; - enlargeEmojiOnlyMessages_ = new Toggle{this}; - sortByImportance_ = new Toggle{this}; - readReceipts_ = new Toggle{this}; - markdown_ = new Toggle{this}; - desktopNotifications_ = new Toggle{this}; - alertOnNotification_ = new Toggle{this}; - useStunServer_ = new Toggle{this}; - mobileMode_ = new Toggle{this}; - scaleFactorCombo_ = new QComboBox{this}; - fontSizeCombo_ = new QComboBox{this}; - fontSelectionCombo_ = new QFontComboBox{this}; - emojiFontSelectionCombo_ = new QComboBox{this}; - ringtoneCombo_ = new QComboBox{this}; - microphoneCombo_ = new QComboBox{this}; - cameraCombo_ = new QComboBox{this}; - cameraResolutionCombo_ = new QComboBox{this}; - cameraFrameRateCombo_ = new QComboBox{this}; - timelineMaxWidthSpin_ = new QSpinBox{this}; - privacyScreenTimeout_ = new QSpinBox{this}; + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + useIdenticon_ = new Toggle{this}; + decryptSidebar_ = new Toggle(this); + privacyScreen_ = new Toggle{this}; + onlyShareKeysWithVerifiedUsers_ = new Toggle(this); + shareKeysWithTrustedUsers_ = new Toggle(this); + useOnlineKeyBackup_ = new Toggle(this); + groupViewToggle_ = new Toggle{this}; + timelineButtonsToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + messageHoverHighlight_ = new Toggle{this}; + enlargeEmojiOnlyMessages_ = new Toggle{this}; + sortByImportance_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdown_ = new Toggle{this}; + animateImagesOnHover_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + alertOnNotification_ = new Toggle{this}; + useStunServer_ = new Toggle{this}; + mobileMode_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QFontComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; + ringtoneCombo_ = new QComboBox{this}; + microphoneCombo_ = new QComboBox{this}; + cameraCombo_ = new QComboBox{this}; + cameraResolutionCombo_ = new QComboBox{this}; + cameraFrameRateCombo_ = new QComboBox{this}; + timelineMaxWidthSpin_ = new QSpinBox{this}; + privacyScreenTimeout_ = new QSpinBox{this}; - trayToggle_->setChecked(settings_->tray()); - startInTrayToggle_->setChecked(settings_->startInTray()); - avatarCircles_->setChecked(settings_->avatarCircles()); - decryptSidebar_->setChecked(settings_->decryptSidebar()); - privacyScreen_->setChecked(settings_->privacyScreen()); - onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); - shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); - groupViewToggle_->setChecked(settings_->groupView()); - timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); - typingNotifications_->setChecked(settings_->typingNotifications()); - messageHoverHighlight_->setChecked(settings_->messageHoverHighlight()); - enlargeEmojiOnlyMessages_->setChecked(settings_->enlargeEmojiOnlyMessages()); - sortByImportance_->setChecked(settings_->sortByImportance()); - readReceipts_->setChecked(settings_->readReceipts()); - markdown_->setChecked(settings_->markdown()); - desktopNotifications_->setChecked(settings_->hasDesktopNotifications()); - alertOnNotification_->setChecked(settings_->hasAlertOnNotification()); - useStunServer_->setChecked(settings_->useStunServer()); - mobileMode_->setChecked(settings_->mobileMode()); + trayToggle_->setChecked(settings_->tray()); + startInTrayToggle_->setChecked(settings_->startInTray()); + avatarCircles_->setChecked(settings_->avatarCircles()); + useIdenticon_->setChecked(settings_->useIdenticon()); + decryptSidebar_->setChecked(settings_->decryptSidebar()); + privacyScreen_->setChecked(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); + shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); + useOnlineKeyBackup_->setChecked(settings_->useOnlineKeyBackup()); + groupViewToggle_->setChecked(settings_->groupView()); + timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); + typingNotifications_->setChecked(settings_->typingNotifications()); + messageHoverHighlight_->setChecked(settings_->messageHoverHighlight()); + enlargeEmojiOnlyMessages_->setChecked(settings_->enlargeEmojiOnlyMessages()); + sortByImportance_->setChecked(settings_->sortByImportance()); + readReceipts_->setChecked(settings_->readReceipts()); + markdown_->setChecked(settings_->markdown()); + animateImagesOnHover_->setChecked(settings_->animateImagesOnHover()); + desktopNotifications_->setChecked(settings_->hasDesktopNotifications()); + alertOnNotification_->setChecked(settings_->hasAlertOnNotification()); + useStunServer_->setChecked(settings_->useStunServer()); + mobileMode_->setChecked(settings_->mobileMode()); - if (!settings_->tray()) { - startInTrayToggle_->setState(false); - startInTrayToggle_->setDisabled(true); + if (!settings_->tray()) { + startInTrayToggle_->setState(false); + startInTrayToggle_->setDisabled(true); + } + + if (!settings_->privacyScreen()) { + privacyScreenTimeout_->setDisabled(true); + } + + avatarCircles_->setFixedSize(64, 48); + + auto uiLabel_ = new QLabel{tr("INTERFACE"), this}; + uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin); + uiLabel_->setAlignment(Qt::AlignBottom); + uiLabel_->setFont(font); + + for (double option = 1; option <= 3; option += 0.25) + scaleFactorCombo_->addItem(QString::number(option)); + for (double option = 6; option <= 24; option += 0.5) + fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option))); + + QFontDatabase fontDb; + + // TODO: Is there a way to limit to just emojis, rather than + // all emoji fonts? + auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); + emojiFontSelectionCombo_->addItem(tr("Default")); + for (const auto &family : emojiFamilies) { + emojiFontSelectionCombo_->addItem(family); + } + + QString currentFont = settings_->font(); + if (currentFont != "default" || currentFont != "") { + fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont)); + } + + emojiFontSelectionCombo_->setCurrentIndex( + emojiFontSelectionCombo_->findText(settings_->emojiFont())); + + themeCombo_ = new QComboBox{this}; + themeCombo_->addItem("Light"); + themeCombo_->addItem("Dark"); + themeCombo_->addItem("System"); + + QString themeStr = settings_->theme(); + themeStr.replace(0, 1, themeStr[0].toUpper()); + int themeIndex = themeCombo_->findText(themeStr); + themeCombo_->setCurrentIndex(themeIndex); + + timelineMaxWidthSpin_->setMinimum(0); + timelineMaxWidthSpin_->setMaximum(100'000'000); + timelineMaxWidthSpin_->setSingleStep(10); + + privacyScreenTimeout_->setMinimum(0); + privacyScreenTimeout_->setMaximum(3600); + privacyScreenTimeout_->setSingleStep(10); + + auto callsLabel = new QLabel{tr("CALLS"), this}; + callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin); + callsLabel->setAlignment(Qt::AlignBottom); + callsLabel->setFont(font); + + auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; + encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); + encryptionLabel_->setAlignment(Qt::AlignBottom); + encryptionLabel_->setFont(font); + + QFont monospaceFont; + monospaceFont.setFamily("Monospace"); + monospaceFont.setStyleHint(QFont::Monospace); + monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); + + deviceIdValue_ = new QLabel{this}; + deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); + deviceIdValue_->setFont(monospaceFont); + + deviceFingerprintValue_ = new QLabel{this}; + deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); + deviceFingerprintValue_->setFont(monospaceFont); + + deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); + + backupSecretCached = new QLabel{this}; + masterSecretCached = new QLabel{this}; + selfSigningSecretCached = new QLabel{this}; + userSigningSecretCached = new QLabel{this}; + backupSecretCached->setFont(monospaceFont); + masterSecretCached->setFont(monospaceFont); + selfSigningSecretCached->setFont(monospaceFont); + userSigningSecretCached->setFont(monospaceFont); + + auto sessionKeysLabel = new QLabel{tr("Session Keys"), this}; + sessionKeysLabel->setFont(font); + sessionKeysLabel->setMargin(OptionMargin); + + auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; + auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; + + auto sessionKeysLayout = new QHBoxLayout; + sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); + sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); + sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); + + auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this}; + crossSigningKeysLabel->setFont(font); + crossSigningKeysLabel->setMargin(OptionMargin); + + auto crossSigningRequestBtn = new QPushButton{tr("REQUEST"), this}; + auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this}; + + auto crossSigningKeysLayout = new QHBoxLayout; + crossSigningKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); + crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight); + crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight); + + auto boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") { + auto label = new QLabel{labelText, this}; + label->setFont(font); + label->setMargin(OptionMargin); + + if (!tooltipText.isEmpty()) { + label->setToolTip(tooltipText); } - if (!settings_->privacyScreen()) { - privacyScreenTimeout_->setDisabled(true); - } + auto layout = new QHBoxLayout; + layout->addWidget(field, 0, Qt::AlignRight); - avatarCircles_->setFixedSize(64, 48); + formLayout_->addRow(label, layout); + }; - auto uiLabel_ = new QLabel{tr("INTERFACE"), this}; - uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin); - uiLabel_->setAlignment(Qt::AlignBottom); - uiLabel_->setFont(font); + formLayout_->addRow(general_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Minimize to tray"), + trayToggle_, + tr("Keep the application running in the background after closing the client window.")); + boxWrap(tr("Start in tray"), + startInTrayToggle_, + tr("Start the application in the background without showing the client window.")); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Circular Avatars"), + avatarCircles_, + tr("Change the appearance of user avatars in chats.\nOFF - square, ON - Circle.")); + boxWrap(tr("Use identicons"), + useIdenticon_, + tr("Display an identicon instead of a letter when a user has not set an avatar.")); + boxWrap(tr("Group's sidebar"), + groupViewToggle_, + tr("Show a column containing groups and tags next to the room list.")); + boxWrap(tr("Decrypt messages in sidebar"), + decryptSidebar_, + tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in " + "encrypted chats.")); + boxWrap(tr("Privacy Screen"), + privacyScreen_, + tr("When the window loses focus, the timeline will\nbe blurred.")); + boxWrap(tr("Privacy screen timeout (in seconds [0 - 3600])"), + privacyScreenTimeout_, + tr("Set timeout (in seconds) for how long after window loses\nfocus before the screen" + " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 " + "hour (3600 seconds)")); + boxWrap(tr("Show buttons in timeline"), + timelineButtonsToggle_, + tr("Show buttons to quickly reply, react or access additional options next to each " + "message.")); + boxWrap(tr("Limit width of timeline"), + timelineMaxWidthSpin_, + tr("Set the max width of messages in the timeline (in pixels). This can help " + "readability on wide screen, when Nheko is maximised")); + boxWrap(tr("Typing notifications"), + typingNotifications_, + tr("Show who is typing in a room.\nThis will also enable or disable sending typing " + "notifications to others.")); + boxWrap( + tr("Sort rooms by unreads"), + sortByImportance_, + tr("Display rooms with new messages first.\nIf this is off, the list of rooms will only " + "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms which " + "have active notifications (the small circle with a number in it) will be sorted on " + "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't " + "seem to consider them as important as the other rooms.")); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Read receipts"), + readReceipts_, + tr("Show if your message was read.\nStatus is displayed next to timestamps.")); + boxWrap(tr("Send messages as Markdown"), + markdown_, + tr("Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " + "text.")); + boxWrap(tr("Play animated images only on hover"), + animateImagesOnHover_, + tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.")); + boxWrap(tr("Desktop notifications"), + desktopNotifications_, + tr("Notify about received message when the client is not currently focused.")); + boxWrap(tr("Alert on notification"), + alertOnNotification_, + tr("Show an alert when a message is received.\nThis usually causes the application " + "icon in the task bar to animate in some fashion.")); + boxWrap(tr("Highlight message on hover"), + messageHoverHighlight_, + tr("Change the background color of messages when you hover over them.")); + boxWrap(tr("Large Emoji in timeline"), + enlargeEmojiOnlyMessages_, + tr("Make font size larger if messages with only a few emojis are displayed.")); + formLayout_->addRow(uiLabel_); + formLayout_->addRow(new HorizontalLine{this}); - for (double option = 1; option <= 3; option += 0.25) - scaleFactorCombo_->addItem(QString::number(option)); - for (double option = 6; option <= 24; option += 0.5) - fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option))); - - QFontDatabase fontDb; - - // TODO: Is there a way to limit to just emojis, rather than - // all emoji fonts? - auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); - emojiFontSelectionCombo_->addItem(tr("Default")); - for (const auto &family : emojiFamilies) { - emojiFontSelectionCombo_->addItem(family); - } - - QString currentFont = settings_->font(); - if (currentFont != "default" || currentFont != "") { - fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont)); - } - - emojiFontSelectionCombo_->setCurrentIndex( - emojiFontSelectionCombo_->findText(settings_->emojiFont())); - - themeCombo_ = new QComboBox{this}; - themeCombo_->addItem("Light"); - themeCombo_->addItem("Dark"); - themeCombo_->addItem("System"); - - QString themeStr = settings_->theme(); - themeStr.replace(0, 1, themeStr[0].toUpper()); - int themeIndex = themeCombo_->findText(themeStr); - themeCombo_->setCurrentIndex(themeIndex); - - timelineMaxWidthSpin_->setMinimum(0); - timelineMaxWidthSpin_->setMaximum(100'000'000); - timelineMaxWidthSpin_->setSingleStep(10); - - privacyScreenTimeout_->setMinimum(0); - privacyScreenTimeout_->setMaximum(3600); - privacyScreenTimeout_->setSingleStep(10); - - auto callsLabel = new QLabel{tr("CALLS"), this}; - callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin); - callsLabel->setAlignment(Qt::AlignBottom); - callsLabel->setFont(font); - - auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; - encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); - encryptionLabel_->setAlignment(Qt::AlignBottom); - encryptionLabel_->setFont(font); - - QFont monospaceFont; - monospaceFont.setFamily("Monospace"); - monospaceFont.setStyleHint(QFont::Monospace); - monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); - - deviceIdValue_ = new QLabel{this}; - deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); - deviceIdValue_->setFont(monospaceFont); - - deviceFingerprintValue_ = new QLabel{this}; - deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); - deviceFingerprintValue_->setFont(monospaceFont); - - deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); - - backupSecretCached = new QLabel{this}; - masterSecretCached = new QLabel{this}; - selfSigningSecretCached = new QLabel{this}; - userSigningSecretCached = new QLabel{this}; - backupSecretCached->setFont(monospaceFont); - masterSecretCached->setFont(monospaceFont); - selfSigningSecretCached->setFont(monospaceFont); - userSigningSecretCached->setFont(monospaceFont); - - auto sessionKeysLabel = new QLabel{tr("Session Keys"), this}; - sessionKeysLabel->setFont(font); - sessionKeysLabel->setMargin(OptionMargin); - - auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; - auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; - - auto sessionKeysLayout = new QHBoxLayout; - sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); - sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); - sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); - - auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this}; - crossSigningKeysLabel->setFont(font); - crossSigningKeysLabel->setMargin(OptionMargin); - - auto crossSigningRequestBtn = new QPushButton{tr("REQUEST"), this}; - auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this}; - - auto crossSigningKeysLayout = new QHBoxLayout; - crossSigningKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); - crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight); - crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight); - - auto boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") { - auto label = new QLabel{labelText, this}; - label->setFont(font); - label->setMargin(OptionMargin); - - if (!tooltipText.isEmpty()) { - label->setToolTip(tooltipText); - } - - auto layout = new QHBoxLayout; - layout->addWidget(field, 0, Qt::AlignRight); - - formLayout_->addRow(label, layout); - }; - - formLayout_->addRow(general_); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap( - tr("Minimize to tray"), - trayToggle_, - tr("Keep the application running in the background after closing the client window.")); - boxWrap(tr("Start in tray"), - startInTrayToggle_, - tr("Start the application in the background without showing the client window.")); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Circular Avatars"), - avatarCircles_, - tr("Change the appearance of user avatars in chats.\nOFF - square, ON - Circle.")); - boxWrap(tr("Group's sidebar"), - groupViewToggle_, - tr("Show a column containing groups and tags next to the room list.")); - boxWrap(tr("Decrypt messages in sidebar"), - decryptSidebar_, - tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in " - "encrypted chats.")); - boxWrap(tr("Privacy Screen"), - privacyScreen_, - tr("When the window loses focus, the timeline will\nbe blurred.")); - boxWrap( - tr("Privacy screen timeout (in seconds [0 - 3600])"), - privacyScreenTimeout_, - tr("Set timeout (in seconds) for how long after window loses\nfocus before the screen" - " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 " - "hour (3600 seconds)")); - boxWrap(tr("Show buttons in timeline"), - timelineButtonsToggle_, - tr("Show buttons to quickly reply, react or access additional options next to each " - "message.")); - boxWrap(tr("Limit width of timeline"), - timelineMaxWidthSpin_, - tr("Set the max width of messages in the timeline (in pixels). This can help " - "readability on wide screen, when Nheko is maximised")); - boxWrap(tr("Typing notifications"), - typingNotifications_, - tr("Show who is typing in a room.\nThis will also enable or disable sending typing " - "notifications to others.")); - boxWrap( - tr("Sort rooms by unreads"), - sortByImportance_, - tr( - "Display rooms with new messages first.\nIf this is off, the list of rooms will only " - "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms which " - "have active notifications (the small circle with a number in it) will be sorted on " - "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't " - "seem to consider them as important as the other rooms.")); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Read receipts"), - readReceipts_, - tr("Show if your message was read.\nStatus is displayed next to timestamps.")); - boxWrap( - tr("Send messages as Markdown"), - markdown_, - tr("Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " - "text.")); - boxWrap(tr("Desktop notifications"), - desktopNotifications_, - tr("Notify about received message when the client is not currently focused.")); - boxWrap(tr("Alert on notification"), - alertOnNotification_, - tr("Show an alert when a message is received.\nThis usually causes the application " - "icon in the task bar to animate in some fashion.")); - boxWrap(tr("Highlight message on hover"), - messageHoverHighlight_, - tr("Change the background color of messages when you hover over them.")); - boxWrap(tr("Large Emoji in timeline"), - enlargeEmojiOnlyMessages_, - tr("Make font size larger if messages with only a few emojis are displayed.")); - formLayout_->addRow(uiLabel_); - formLayout_->addRow(new HorizontalLine{this}); - - boxWrap(tr("Touchscreen mode"), - mobileMode_, - tr("Will prevent text selection in the timeline to make touch scrolling easier.")); + boxWrap(tr("Touchscreen mode"), + mobileMode_, + tr("Will prevent text selection in the timeline to make touch scrolling easier.")); #if !defined(Q_OS_MAC) - boxWrap(tr("Scale factor"), - scaleFactorCombo_, - tr("Change the scale factor of the whole user interface.")); + boxWrap(tr("Scale factor"), + scaleFactorCombo_, + tr("Change the scale factor of the whole user interface.")); #else - scaleFactorCombo_->hide(); + scaleFactorCombo_->hide(); #endif - boxWrap(tr("Font size"), fontSizeCombo_); - boxWrap(tr("Font Family"), fontSelectionCombo_); + boxWrap(tr("Font size"), fontSizeCombo_); + boxWrap(tr("Font Family"), fontSelectionCombo_); #if !defined(Q_OS_MAC) - boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_); + boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_); #else - emojiFontSelectionCombo_->hide(); + emojiFontSelectionCombo_->hide(); #endif - boxWrap(tr("Theme"), themeCombo_); + boxWrap(tr("Theme"), themeCombo_); - formLayout_->addRow(callsLabel); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Ringtone"), - ringtoneCombo_, - tr("Set the notification sound to play when a call invite arrives")); - boxWrap(tr("Microphone"), microphoneCombo_); - boxWrap(tr("Camera"), cameraCombo_); - boxWrap(tr("Camera resolution"), cameraResolutionCombo_); - boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_); + formLayout_->addRow(callsLabel); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Ringtone"), + ringtoneCombo_, + tr("Set the notification sound to play when a call invite arrives")); + boxWrap(tr("Microphone"), microphoneCombo_); + boxWrap(tr("Camera"), cameraCombo_); + boxWrap(tr("Camera resolution"), cameraResolutionCombo_); + boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_); - ringtoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - ringtoneCombo_->addItem("Mute"); - ringtoneCombo_->addItem("Default"); - ringtoneCombo_->addItem("Other..."); - const QString &ringtone = settings_->ringtone(); - if (!ringtone.isEmpty() && ringtone != "Mute" && ringtone != "Default") - ringtoneCombo_->addItem(ringtone); - microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + ringtoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + ringtoneCombo_->addItem("Mute"); + ringtoneCombo_->addItem("Default"); + ringtoneCombo_->addItem("Other..."); + const QString &ringtone = settings_->ringtone(); + if (!ringtone.isEmpty() && ringtone != "Mute" && ringtone != "Default") + ringtoneCombo_->addItem(ringtone); + microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - boxWrap(tr("Allow fallback call assist server"), - useStunServer_, - tr("Will use turn.matrix.org as assist when your home server does not offer one.")); + boxWrap(tr("Allow fallback call assist server"), + useStunServer_, + tr("Will use turn.matrix.org as assist when your home server does not offer one.")); - formLayout_->addRow(encryptionLabel_); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Device ID"), deviceIdValue_); - boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); - boxWrap(tr("Send encrypted messages to verified users only"), - onlyShareKeysWithVerifiedUsers_, - tr("Requires a user to be verified to send encrypted messages to them. This " - "improves safety but makes E2EE more tedious.")); - boxWrap(tr("Share keys with verified users and devices"), - shareKeysWithTrustedUsers_, - tr("Automatically replies to key requests from other users, if they are verified, " - "even if that device shouldn't have access to those keys otherwise.")); - formLayout_->addRow(new HorizontalLine{this}); - formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); - formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); + formLayout_->addRow(encryptionLabel_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Device ID"), deviceIdValue_); + boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); + boxWrap(tr("Send encrypted messages to verified users only"), + onlyShareKeysWithVerifiedUsers_, + tr("Requires a user to be verified to send encrypted messages to them. This " + "improves safety but makes E2EE more tedious.")); + boxWrap(tr("Share keys with verified users and devices"), + shareKeysWithTrustedUsers_, + tr("Automatically replies to key requests from other users, if they are verified, " + "even if that device shouldn't have access to those keys otherwise.")); + boxWrap(tr("Online Key Backup"), + useOnlineKeyBackup_, + tr("Download message encryption keys from and upload to the encrypted online key " + "backup.")); + formLayout_->addRow(new HorizontalLine{this}); + formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); + formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); - boxWrap(tr("Master signing key"), - masterSecretCached, - tr("Your most important key. You don't need to have it cached, since not caching " - "it makes it less likely it can be stolen and it is only needed to rotate your " - "other signing keys.")); - boxWrap(tr("User signing key"), - userSigningSecretCached, - tr("The key to verify other users. If it is cached, verifying a user will verify " - "all their devices.")); - boxWrap( - tr("Self signing key"), - selfSigningSecretCached, - tr("The key to verify your own devices. If it is cached, verifying one of your devices " - "will mark it verified for all your other devices and for users, that have verified " - "you.")); - boxWrap(tr("Backup key"), - backupSecretCached, - tr("The key to decrypt online key backups. If it is cached, you can enable online " - "key backup to store encryption keys securely encrypted on the server.")); - updateSecretStatus(); + boxWrap(tr("Master signing key"), + masterSecretCached, + tr("Your most important key. You don't need to have it cached, since not caching " + "it makes it less likely it can be stolen and it is only needed to rotate your " + "other signing keys.")); + boxWrap(tr("User signing key"), + userSigningSecretCached, + tr("The key to verify other users. If it is cached, verifying a user will verify " + "all their devices.")); + boxWrap(tr("Self signing key"), + selfSigningSecretCached, + tr("The key to verify your own devices. If it is cached, verifying one of your devices " + "will mark it verified for all your other devices and for users, that have verified " + "you.")); + boxWrap(tr("Backup key"), + backupSecretCached, + tr("The key to decrypt online key backups. If it is cached, you can enable online " + "key backup to store encryption keys securely encrypted on the server.")); + updateSecretStatus(); - auto scrollArea_ = new QScrollArea{this}; - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); + auto scrollArea_ = new QScrollArea{this}; + scrollArea_->setFrameShape(QFrame::NoFrame); + scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + scrollArea_->setWidgetResizable(true); + scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); - QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); + QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); - auto spacingAroundForm = new QHBoxLayout; - spacingAroundForm->addStretch(1); - spacingAroundForm->addLayout(formLayout_, 0); - spacingAroundForm->addStretch(1); + auto spacingAroundForm = new QHBoxLayout; + spacingAroundForm->addStretch(1); + spacingAroundForm->addLayout(formLayout_, 0); + spacingAroundForm->addStretch(1); - auto scrollAreaContents_ = new QWidget{this}; - scrollAreaContents_->setObjectName("UserSettingScrollWidget"); - scrollAreaContents_->setLayout(spacingAroundForm); + auto scrollAreaContents_ = new QWidget{this}; + scrollAreaContents_->setObjectName("UserSettingScrollWidget"); + scrollAreaContents_->setLayout(spacingAroundForm); - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addLayout(topBarLayout_); - topLayout_->addWidget(scrollArea_, Qt::AlignTop); - topLayout_->addStretch(1); - topLayout_->addWidget(versionInfo); + scrollArea_->setWidget(scrollAreaContents_); + topLayout_->addLayout(topBarLayout_); + topLayout_->addWidget(scrollArea_, Qt::AlignTop); + topLayout_->addStretch(1); + topLayout_->addWidget(versionInfo); - connect(themeCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &text) { - settings_->setTheme(text.toLower()); - emit themeChanged(); - }); - connect(scaleFactorCombo_, - static_cast(&QComboBox::currentTextChanged), - [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); }); - connect(fontSizeCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); }); - connect(fontSelectionCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); - connect(emojiFontSelectionCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); + connect(themeCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &text) { + settings_->setTheme(text.toLower()); + emit themeChanged(); + }); + connect(scaleFactorCombo_, + static_cast(&QComboBox::currentTextChanged), + [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); }); + connect(fontSizeCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); }); + connect(fontSelectionCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); + connect(emojiFontSelectionCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); - connect(ringtoneCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &ringtone) { - if (ringtone == "Other...") { - QString homeFolder = - QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - auto filepath = QFileDialog::getOpenFileName( - this, tr("Select a file"), homeFolder, tr("All Files (*)")); - if (!filepath.isEmpty()) { - const auto &oldSetting = settings_->ringtone(); - if (oldSetting != "Mute" && oldSetting != "Default") - ringtoneCombo_->removeItem( - ringtoneCombo_->findText(oldSetting)); - settings_->setRingtone(filepath); - ringtoneCombo_->addItem(filepath); - ringtoneCombo_->setCurrentText(filepath); - } else { - ringtoneCombo_->setCurrentText(settings_->ringtone()); - } - } else if (ringtone == "Mute" || ringtone == "Default") { - const auto &oldSetting = settings_->ringtone(); - if (oldSetting != "Mute" && oldSetting != "Default") - ringtoneCombo_->removeItem( - ringtoneCombo_->findText(oldSetting)); - settings_->setRingtone(ringtone); - } - }); - - connect(microphoneCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString µphone) { settings_->setMicrophone(microphone); }); - - connect(cameraCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &camera) { - settings_->setCamera(camera); - std::vector resolutions = - CallDevices::instance().resolutions(camera.toStdString()); - cameraResolutionCombo_->clear(); - for (const auto &resolution : resolutions) - cameraResolutionCombo_->addItem(QString::fromStdString(resolution)); - }); - - connect(cameraResolutionCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &resolution) { - settings_->setCameraResolution(resolution); - std::vector frameRates = CallDevices::instance().frameRates( - settings_->camera().toStdString(), resolution.toStdString()); - cameraFrameRateCombo_->clear(); - for (const auto &frameRate : frameRates) - cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate)); - }); - - connect(cameraFrameRateCombo_, - static_cast(&QComboBox::currentTextChanged), - [this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); }); - - connect(trayToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setTray(enabled); - if (enabled) { - startInTrayToggle_->setChecked(false); - startInTrayToggle_->setEnabled(true); - startInTrayToggle_->setState(false); - settings_->setStartInTray(false); - } else { - startInTrayToggle_->setChecked(false); - startInTrayToggle_->setState(false); - startInTrayToggle_->setDisabled(true); - settings_->setStartInTray(false); + connect(ringtoneCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &ringtone) { + if (ringtone == "Other...") { + QString homeFolder = + QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + auto filepath = QFileDialog::getOpenFileName( + this, tr("Select a file"), homeFolder, tr("All Files (*)")); + if (!filepath.isEmpty()) { + const auto &oldSetting = settings_->ringtone(); + if (oldSetting != "Mute" && oldSetting != "Default") + ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting)); + settings_->setRingtone(filepath); + ringtoneCombo_->addItem(filepath); + ringtoneCombo_->setCurrentText(filepath); + } else { + ringtoneCombo_->setCurrentText(settings_->ringtone()); + } + } else if (ringtone == "Mute" || ringtone == "Default") { + const auto &oldSetting = settings_->ringtone(); + if (oldSetting != "Mute" && oldSetting != "Default") + ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting)); + settings_->setRingtone(ringtone); } - emit trayOptionChanged(enabled); + }); + + connect(microphoneCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString µphone) { settings_->setMicrophone(microphone); }); + + connect(cameraCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &camera) { + settings_->setCamera(camera); + std::vector resolutions = + CallDevices::instance().resolutions(camera.toStdString()); + cameraResolutionCombo_->clear(); + for (const auto &resolution : resolutions) + cameraResolutionCombo_->addItem(QString::fromStdString(resolution)); + }); + + connect(cameraResolutionCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &resolution) { + settings_->setCameraResolution(resolution); + std::vector frameRates = CallDevices::instance().frameRates( + settings_->camera().toStdString(), resolution.toStdString()); + cameraFrameRateCombo_->clear(); + for (const auto &frameRate : frameRates) + cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate)); + }); + + connect(cameraFrameRateCombo_, + static_cast(&QComboBox::currentTextChanged), + [this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); }); + + connect(trayToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setTray(enabled); + if (enabled) { + startInTrayToggle_->setChecked(false); + startInTrayToggle_->setEnabled(true); + startInTrayToggle_->setState(false); + settings_->setStartInTray(false); + } else { + startInTrayToggle_->setChecked(false); + startInTrayToggle_->setState(false); + startInTrayToggle_->setDisabled(true); + settings_->setStartInTray(false); + } + emit trayOptionChanged(enabled); + }); + + connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setStartInTray(enabled); + }); + + connect(mobileMode_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setMobileMode(enabled); + }); + + connect(groupViewToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setGroupView(enabled); + }); + + connect(decryptSidebar_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setDecryptSidebar(enabled); + }); + + connect(privacyScreen_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setPrivacyScreen(enabled); + if (enabled) { + privacyScreenTimeout_->setEnabled(true); + } else { + privacyScreenTimeout_->setDisabled(true); + } + }); + + connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setOnlyShareKeysWithVerifiedUsers(enabled); + }); + + connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setShareKeysWithTrustedUsers(enabled); + }); + + connect(useOnlineKeyBackup_, &Toggle::toggled, this, [this](bool enabled) { + if (enabled) { + if (QMessageBox::question( + this, + tr("Enable online key backup"), + tr("The Nheko authors recommend not enabling online key backup until " + "symmetric online key backup is available. Enable anyway?")) != + QMessageBox::StandardButton::Yes) { + useOnlineKeyBackup_->setState(false); + return; + } + } + settings_->setUseOnlineKeyBackup(enabled); + }); + + connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setAvatarCircles(enabled); + }); + + if (JdenticonProvider::isAvailable()) + connect(useIdenticon_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setUseIdenticon(enabled); }); + else + useIdenticon_->setDisabled(true); - connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setStartInTray(enabled); - }); + connect( + markdown_, &Toggle::toggled, this, [this](bool enabled) { settings_->setMarkdown(enabled); }); - connect(mobileMode_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMobileMode(enabled); - }); + connect(animateImagesOnHover_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setAnimateImagesOnHover(enabled); + }); - connect(groupViewToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setGroupView(enabled); - }); + connect(typingNotifications_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setTypingNotifications(enabled); + }); - connect(decryptSidebar_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setDecryptSidebar(enabled); - }); + connect(sortByImportance_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setSortByImportance(enabled); + }); - connect(privacyScreen_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setPrivacyScreen(enabled); - if (enabled) { - privacyScreenTimeout_->setEnabled(true); - } else { - privacyScreenTimeout_->setDisabled(true); - } - }); + connect(timelineButtonsToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setButtonsInTimeline(enabled); + }); - connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setOnlyShareKeysWithVerifiedUsers(enabled); - }); + connect(readReceipts_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setReadReceipts(enabled); + }); - connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setShareKeysWithTrustedUsers(enabled); - }); + connect(desktopNotifications_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setDesktopNotifications(enabled); + }); - connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAvatarCircles(enabled); - }); + connect(alertOnNotification_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setAlertOnNotification(enabled); + }); - connect(markdown_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMarkdown(enabled); - }); + connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setMessageHoverHighlight(enabled); + }); - connect(typingNotifications_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setTypingNotifications(enabled); - }); + connect(enlargeEmojiOnlyMessages_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setEnlargeEmojiOnlyMessages(enabled); + }); - connect(sortByImportance_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setSortByImportance(enabled); - }); + connect(useStunServer_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setUseStunServer(enabled); + }); - connect(timelineButtonsToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setButtonsInTimeline(enabled); - }); + connect(timelineMaxWidthSpin_, + qOverload(&QSpinBox::valueChanged), + this, + [this](int newValue) { settings_->setTimelineMaxWidth(newValue); }); - connect(readReceipts_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setReadReceipts(enabled); - }); + connect(privacyScreenTimeout_, + qOverload(&QSpinBox::valueChanged), + this, + [this](int newValue) { settings_->setPrivacyScreenTimeout(newValue); }); - connect(desktopNotifications_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setDesktopNotifications(enabled); - }); + connect( + sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); - connect(alertOnNotification_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAlertOnNotification(enabled); - }); + connect( + sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); - connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMessageHoverHighlight(enabled); - }); + connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() { + olm::request_cross_signing_keys(); + }); - connect(enlargeEmojiOnlyMessages_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setEnlargeEmojiOnlyMessages(enabled); - }); + connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() { + olm::download_cross_signing_keys(); + }); - connect(useStunServer_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setUseStunServer(enabled); - }); - - connect(timelineMaxWidthSpin_, - qOverload(&QSpinBox::valueChanged), - this, - [this](int newValue) { settings_->setTimelineMaxWidth(newValue); }); - - connect(privacyScreenTimeout_, - qOverload(&QSpinBox::valueChanged), - this, - [this](int newValue) { settings_->setPrivacyScreenTimeout(newValue); }); - - connect( - sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); - - connect( - sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); - - connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() { - olm::request_cross_signing_keys(); - }); - - connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() { - olm::download_cross_signing_keys(); - }); - - connect(backBtn_, &QPushButton::clicked, this, [this]() { - settings_->save(); - emit moveBack(); - }); + connect(backBtn_, &QPushButton::clicked, this, [this]() { + settings_->save(); + emit moveBack(); + }); } void UserSettingsPage::showEvent(QShowEvent *) { - // FIXME macOS doesn't show the full option unless a space is added. - utils::restoreCombobox(fontSizeCombo_, QString::number(settings_->fontSize()) + " "); - utils::restoreCombobox(scaleFactorCombo_, QString::number(utils::scaleFactor())); - utils::restoreCombobox(themeCombo_, settings_->theme()); - utils::restoreCombobox(ringtoneCombo_, settings_->ringtone()); + // FIXME macOS doesn't show the full option unless a space is added. + utils::restoreCombobox(fontSizeCombo_, QString::number(settings_->fontSize()) + " "); + utils::restoreCombobox(scaleFactorCombo_, QString::number(utils::scaleFactor())); + utils::restoreCombobox(themeCombo_, settings_->theme()); + utils::restoreCombobox(ringtoneCombo_, settings_->ringtone()); - trayToggle_->setState(settings_->tray()); - startInTrayToggle_->setState(settings_->startInTray()); - groupViewToggle_->setState(settings_->groupView()); - decryptSidebar_->setState(settings_->decryptSidebar()); - privacyScreen_->setState(settings_->privacyScreen()); - onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); - shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); - avatarCircles_->setState(settings_->avatarCircles()); - typingNotifications_->setState(settings_->typingNotifications()); - sortByImportance_->setState(settings_->sortByImportance()); - timelineButtonsToggle_->setState(settings_->buttonsInTimeline()); - mobileMode_->setState(settings_->mobileMode()); - readReceipts_->setState(settings_->readReceipts()); - markdown_->setState(settings_->markdown()); - desktopNotifications_->setState(settings_->hasDesktopNotifications()); - alertOnNotification_->setState(settings_->hasAlertOnNotification()); - messageHoverHighlight_->setState(settings_->messageHoverHighlight()); - enlargeEmojiOnlyMessages_->setState(settings_->enlargeEmojiOnlyMessages()); - deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); - timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth()); - privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout()); + trayToggle_->setState(settings_->tray()); + startInTrayToggle_->setState(settings_->startInTray()); + groupViewToggle_->setState(settings_->groupView()); + decryptSidebar_->setState(settings_->decryptSidebar()); + privacyScreen_->setState(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); + shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); + useOnlineKeyBackup_->setState(settings_->useOnlineKeyBackup()); + avatarCircles_->setState(settings_->avatarCircles()); + typingNotifications_->setState(settings_->typingNotifications()); + sortByImportance_->setState(settings_->sortByImportance()); + timelineButtonsToggle_->setState(settings_->buttonsInTimeline()); + mobileMode_->setState(settings_->mobileMode()); + readReceipts_->setState(settings_->readReceipts()); + markdown_->setState(settings_->markdown()); + desktopNotifications_->setState(settings_->hasDesktopNotifications()); + alertOnNotification_->setState(settings_->hasAlertOnNotification()); + messageHoverHighlight_->setState(settings_->messageHoverHighlight()); + enlargeEmojiOnlyMessages_->setState(settings_->enlargeEmojiOnlyMessages()); + deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); + timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth()); + privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout()); - auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString()); - microphoneCombo_->clear(); - for (const auto &m : mics) - microphoneCombo_->addItem(QString::fromStdString(m)); + auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString()); + microphoneCombo_->clear(); + for (const auto &m : mics) + microphoneCombo_->addItem(QString::fromStdString(m)); - auto cameraResolution = settings_->cameraResolution(); - auto cameraFrameRate = settings_->cameraFrameRate(); + auto cameraResolution = settings_->cameraResolution(); + auto cameraFrameRate = settings_->cameraFrameRate(); - auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString()); - cameraCombo_->clear(); - for (const auto &c : cameras) - cameraCombo_->addItem(QString::fromStdString(c)); + auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString()); + cameraCombo_->clear(); + for (const auto &c : cameras) + cameraCombo_->addItem(QString::fromStdString(c)); - utils::restoreCombobox(cameraResolutionCombo_, cameraResolution); - utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate); + utils::restoreCombobox(cameraResolutionCombo_, cameraResolution); + utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate); - useStunServer_->setState(settings_->useStunServer()); + useStunServer_->setState(settings_->useStunServer()); - deviceFingerprintValue_->setText( - utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519)); + deviceFingerprintValue_->setText( + utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519)); } void UserSettingsPage::paintEvent(QPaintEvent *) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } void UserSettingsPage::importSessionKeys() { - const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const QString fileName = - QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, ""); + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = + QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, ""); - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, tr("Error"), file.errorString()); - return; - } + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, tr("Error"), file.errorString()); + return; + } - auto bin = file.peek(file.size()); - auto payload = std::string(bin.data(), bin.size()); + auto bin = file.peek(file.size()); + auto payload = std::string(bin.data(), bin.size()); - bool ok; - auto password = QInputDialog::getText(this, - tr("File Password"), - tr("Enter the passphrase to decrypt the file:"), - QLineEdit::Password, - "", - &ok); - if (!ok) - return; + bool ok; + auto password = QInputDialog::getText(this, + tr("File Password"), + tr("Enter the passphrase to decrypt the file:"), + QLineEdit::Password, + "", + &ok); + if (!ok) + return; - if (password.isEmpty()) { - QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); - return; - } + if (password.isEmpty()) { + QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); + return; + } - try { - auto sessions = - mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); - cache::importSessionKeys(std::move(sessions)); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), e.what()); - } + try { + auto sessions = mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); + cache::importSessionKeys(std::move(sessions)); + } catch (const std::exception &e) { + QMessageBox::warning(this, tr("Error"), e.what()); + } } void UserSettingsPage::exportSessionKeys() { - // Open password dialog. - bool ok; - auto password = QInputDialog::getText(this, - tr("File Password"), - tr("Enter passphrase to encrypt your session keys:"), - QLineEdit::Password, - "", - &ok); - if (!ok) - return; + // Open password dialog. + bool ok; + auto password = QInputDialog::getText(this, + tr("File Password"), + tr("Enter passphrase to encrypt your session keys:"), + QLineEdit::Password, + "", + &ok); + if (!ok) + return; - if (password.isEmpty()) { - QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); - return; - } + if (password.isEmpty()) { + QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); + return; + } - // Open file dialog to save the file. - const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const QString fileName = - QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", ""); + // Open file dialog to save the file. + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = + QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", ""); - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::warning(this, tr("Error"), file.errorString()); - return; - } + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, tr("Error"), file.errorString()); + return; + } - // Export sessions & save to file. - try { - auto encrypted_blob = mtx::crypto::encrypt_exported_sessions( - cache::exportSessionKeys(), password.toStdString()); + // Export sessions & save to file. + try { + auto encrypted_blob = mtx::crypto::encrypt_exported_sessions(cache::exportSessionKeys(), + password.toStdString()); - QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob)); + QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob)); - QString prefix("-----BEGIN MEGOLM SESSION DATA-----"); - QString suffix("-----END MEGOLM SESSION DATA-----"); - QString newline("\n"); - QTextStream out(&file); - out << prefix << newline << b64 << newline << suffix << newline; - file.close(); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), e.what()); - } + QString prefix("-----BEGIN MEGOLM SESSION DATA-----"); + QString suffix("-----END MEGOLM SESSION DATA-----"); + QString newline("\n"); + QTextStream out(&file); + out << prefix << newline << b64 << newline << suffix << newline; + file.close(); + } catch (const std::exception &e) { + QMessageBox::warning(this, tr("Error"), e.what()); + } } void UserSettingsPage::updateSecretStatus() { - QString ok = "QLabel { color : #00cc66; }"; - QString notSoOk = "QLabel { color : #ff9933; }"; + QString ok = "QLabel { color : #00cc66; }"; + QString notSoOk = "QLabel { color : #ff9933; }"; - auto updateLabel = [&ok, ¬SoOk](QLabel *label, const std::string &secretName) { - if (cache::secret(secretName)) { - label->setStyleSheet(ok); - label->setText(tr("CACHED")); - } else { - if (secretName == mtx::secret_storage::secrets::cross_signing_master) - label->setStyleSheet(ok); - else - label->setStyleSheet(notSoOk); - label->setText(tr("NOT CACHED")); - } - }; + auto updateLabel = [&ok, ¬SoOk](QLabel *label, const std::string &secretName) { + if (cache::secret(secretName)) { + label->setStyleSheet(ok); + label->setText(tr("CACHED")); + } else { + if (secretName == mtx::secret_storage::secrets::cross_signing_master) + label->setStyleSheet(ok); + else + label->setStyleSheet(notSoOk); + label->setText(tr("NOT CACHED")); + } + }; - updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master); - updateLabel(userSigningSecretCached, - mtx::secret_storage::secrets::cross_signing_user_signing); - updateLabel(selfSigningSecretCached, - mtx::secret_storage::secrets::cross_signing_self_signing); - updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1); + updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master); + updateLabel(userSigningSecretCached, mtx::secret_storage::secrets::cross_signing_user_signing); + updateLabel(selfSigningSecretCached, mtx::secret_storage::secrets::cross_signing_self_signing); + updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1); } diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 84940e47..31e28db2 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -12,6 +12,7 @@ #include #include +#include "JdenticonProvider.h" #include class Toggle; @@ -29,381 +30,395 @@ constexpr int LayoutBottomMargin = LayoutTopMargin; class UserSettings : public QObject { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) - Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE - setMessageHoverHighlight NOTIFY messageHoverHighlightChanged) - Q_PROPERTY(bool enlargeEmojiOnlyMessages READ enlargeEmojiOnlyMessages WRITE - setEnlargeEmojiOnlyMessages NOTIFY enlargeEmojiOnlyMessagesChanged) - Q_PROPERTY(bool tray READ tray WRITE setTray NOTIFY trayChanged) - Q_PROPERTY(bool startInTray READ startInTray WRITE setStartInTray NOTIFY startInTrayChanged) - Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged) - Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged) - Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications - NOTIFY typingNotificationsChanged) - Q_PROPERTY(bool sortByImportance READ sortByImportance WRITE setSortByImportance NOTIFY - roomSortingChanged) - Q_PROPERTY(bool buttonsInTimeline READ buttonsInTimeline WRITE setButtonsInTimeline NOTIFY - buttonInTimelineChanged) - Q_PROPERTY( - bool readReceipts READ readReceipts WRITE setReadReceipts NOTIFY readReceiptsChanged) - Q_PROPERTY(bool desktopNotifications READ hasDesktopNotifications WRITE - setDesktopNotifications NOTIFY desktopNotificationsChanged) - Q_PROPERTY(bool alertOnNotification READ hasAlertOnNotification WRITE setAlertOnNotification - NOTIFY alertOnNotificationChanged) - Q_PROPERTY( - bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged) - Q_PROPERTY(bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY - decryptSidebarChanged) - Q_PROPERTY( - bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged) - Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout - NOTIFY privacyScreenTimeoutChanged) - Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY - timelineMaxWidthChanged) - Q_PROPERTY( - int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged) - Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY - communityListWidthChanged) - Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged) - Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) - Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) - Q_PROPERTY( - QString emojiFont READ emojiFont WRITE setEmojiFontFamily NOTIFY emojiFontChanged) - Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged) - Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged) - Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged) - Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged) - Q_PROPERTY(QString cameraResolution READ cameraResolution WRITE setCameraResolution NOTIFY - cameraResolutionChanged) - Q_PROPERTY(QString cameraFrameRate READ cameraFrameRate WRITE setCameraFrameRate NOTIFY - cameraFrameRateChanged) - Q_PROPERTY(int screenShareFrameRate READ screenShareFrameRate WRITE setScreenShareFrameRate - NOTIFY screenShareFrameRateChanged) - Q_PROPERTY(bool screenSharePiP READ screenSharePiP WRITE setScreenSharePiP NOTIFY - screenSharePiPChanged) - Q_PROPERTY(bool screenShareRemoteVideo READ screenShareRemoteVideo WRITE - setScreenShareRemoteVideo NOTIFY screenShareRemoteVideoChanged) - Q_PROPERTY(bool screenShareHideCursor READ screenShareHideCursor WRITE - setScreenShareHideCursor NOTIFY screenShareHideCursorChanged) - Q_PROPERTY( - bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) - Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE - setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) - Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE - setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) - Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) - Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged) - Q_PROPERTY( - QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged) - Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) - Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) - Q_PROPERTY(bool disableCertificateValidation READ disableCertificateValidation WRITE - setDisableCertificateValidation NOTIFY disableCertificateValidationChanged) + Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE setMessageHoverHighlight + NOTIFY messageHoverHighlightChanged) + Q_PROPERTY(bool enlargeEmojiOnlyMessages READ enlargeEmojiOnlyMessages WRITE + setEnlargeEmojiOnlyMessages NOTIFY enlargeEmojiOnlyMessagesChanged) + Q_PROPERTY(bool tray READ tray WRITE setTray NOTIFY trayChanged) + Q_PROPERTY(bool startInTray READ startInTray WRITE setStartInTray NOTIFY startInTrayChanged) + Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged) + Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged) + Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover + NOTIFY animateImagesOnHoverChanged) + Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications NOTIFY + typingNotificationsChanged) + Q_PROPERTY(bool sortByImportance READ sortByImportance WRITE setSortByImportance NOTIFY + roomSortingChanged) + Q_PROPERTY(bool buttonsInTimeline READ buttonsInTimeline WRITE setButtonsInTimeline NOTIFY + buttonInTimelineChanged) + Q_PROPERTY(bool readReceipts READ readReceipts WRITE setReadReceipts NOTIFY readReceiptsChanged) + Q_PROPERTY(bool desktopNotifications READ hasDesktopNotifications WRITE setDesktopNotifications + NOTIFY desktopNotificationsChanged) + Q_PROPERTY(bool alertOnNotification READ hasAlertOnNotification WRITE setAlertOnNotification + NOTIFY alertOnNotificationChanged) + Q_PROPERTY( + bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged) + Q_PROPERTY( + bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY decryptSidebarChanged) + Q_PROPERTY( + bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged) + Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout + NOTIFY privacyScreenTimeoutChanged) + Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY + timelineMaxWidthChanged) + Q_PROPERTY( + int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged) + Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY + communityListWidthChanged) + Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged) + Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) + Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) + Q_PROPERTY(QString emojiFont READ emojiFont WRITE setEmojiFontFamily NOTIFY emojiFontChanged) + Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged) + Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged) + Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged) + Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged) + Q_PROPERTY(QString cameraResolution READ cameraResolution WRITE setCameraResolution NOTIFY + cameraResolutionChanged) + Q_PROPERTY(QString cameraFrameRate READ cameraFrameRate WRITE setCameraFrameRate NOTIFY + cameraFrameRateChanged) + Q_PROPERTY(int screenShareFrameRate READ screenShareFrameRate WRITE setScreenShareFrameRate + NOTIFY screenShareFrameRateChanged) + Q_PROPERTY( + bool screenSharePiP READ screenSharePiP WRITE setScreenSharePiP NOTIFY screenSharePiPChanged) + Q_PROPERTY(bool screenShareRemoteVideo READ screenShareRemoteVideo WRITE + setScreenShareRemoteVideo NOTIFY screenShareRemoteVideoChanged) + Q_PROPERTY(bool screenShareHideCursor READ screenShareHideCursor WRITE setScreenShareHideCursor + NOTIFY screenShareHideCursorChanged) + Q_PROPERTY( + bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) + Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE + setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) + Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE + setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) + Q_PROPERTY(bool useOnlineKeyBackup READ useOnlineKeyBackup WRITE setUseOnlineKeyBackup NOTIFY + useOnlineKeyBackupChanged) + Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) + Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged) + Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged) + Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) + Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(bool disableCertificateValidation READ disableCertificateValidation WRITE + setDisableCertificateValidation NOTIFY disableCertificateValidationChanged) + Q_PROPERTY(bool useIdenticon READ useIdenticon WRITE setUseIdenticon NOTIFY useIdenticonChanged) - UserSettings(); + UserSettings(); public: - static QSharedPointer instance(); - static void initialize(std::optional profile); + static QSharedPointer instance(); + static void initialize(std::optional profile); - QSettings *qsettings() { return &settings; } + QSettings *qsettings() { return &settings; } - enum class Presence - { - AutomaticPresence, - Online, - Unavailable, - Offline, - }; - Q_ENUM(Presence) + enum class Presence + { + AutomaticPresence, + Online, + Unavailable, + Offline, + }; + Q_ENUM(Presence) - void save(); - void load(std::optional profile); - void applyTheme(); - void setTheme(QString theme); - void setMessageHoverHighlight(bool state); - void setEnlargeEmojiOnlyMessages(bool state); - void setTray(bool state); - void setStartInTray(bool state); - void setMobileMode(bool mode); - void setFontSize(double size); - void setFontFamily(QString family); - void setEmojiFontFamily(QString family); - void setGroupView(bool state); - void setMarkdown(bool state); - void setReadReceipts(bool state); - void setTypingNotifications(bool state); - void setSortByImportance(bool state); - void setButtonsInTimeline(bool state); - void setTimelineMaxWidth(int state); - void setCommunityListWidth(int state); - void setRoomListWidth(int state); - void setDesktopNotifications(bool state); - void setAlertOnNotification(bool state); - void setAvatarCircles(bool state); - void setDecryptSidebar(bool state); - void setPrivacyScreen(bool state); - void setPrivacyScreenTimeout(int state); - void setPresence(Presence state); - void setRingtone(QString ringtone); - void setMicrophone(QString microphone); - void setCamera(QString camera); - void setCameraResolution(QString resolution); - void setCameraFrameRate(QString frameRate); - void setScreenShareFrameRate(int frameRate); - void setScreenSharePiP(bool state); - void setScreenShareRemoteVideo(bool state); - void setScreenShareHideCursor(bool state); - void setUseStunServer(bool state); - void setOnlyShareKeysWithVerifiedUsers(bool state); - void setShareKeysWithTrustedUsers(bool state); - void setProfile(QString profile); - void setUserId(QString userId); - void setAccessToken(QString accessToken); - void setDeviceId(QString deviceId); - void setHomeserver(QString homeserver); - void setDisableCertificateValidation(bool disabled); - void setHiddenTags(QStringList hiddenTags); + void save(); + void load(std::optional profile); + void applyTheme(); + void setTheme(QString theme); + void setMessageHoverHighlight(bool state); + void setEnlargeEmojiOnlyMessages(bool state); + void setTray(bool state); + void setStartInTray(bool state); + void setMobileMode(bool mode); + void setFontSize(double size); + void setFontFamily(QString family); + void setEmojiFontFamily(QString family); + void setGroupView(bool state); + void setMarkdown(bool state); + void setAnimateImagesOnHover(bool state); + void setReadReceipts(bool state); + void setTypingNotifications(bool state); + void setSortByImportance(bool state); + void setButtonsInTimeline(bool state); + void setTimelineMaxWidth(int state); + void setCommunityListWidth(int state); + void setRoomListWidth(int state); + void setDesktopNotifications(bool state); + void setAlertOnNotification(bool state); + void setAvatarCircles(bool state); + void setDecryptSidebar(bool state); + void setPrivacyScreen(bool state); + void setPrivacyScreenTimeout(int state); + void setPresence(Presence state); + void setRingtone(QString ringtone); + void setMicrophone(QString microphone); + void setCamera(QString camera); + void setCameraResolution(QString resolution); + void setCameraFrameRate(QString frameRate); + void setScreenShareFrameRate(int frameRate); + void setScreenSharePiP(bool state); + void setScreenShareRemoteVideo(bool state); + void setScreenShareHideCursor(bool state); + void setUseStunServer(bool state); + void setOnlyShareKeysWithVerifiedUsers(bool state); + void setShareKeysWithTrustedUsers(bool state); + void setUseOnlineKeyBackup(bool state); + void setProfile(QString profile); + void setUserId(QString userId); + void setAccessToken(QString accessToken); + void setDeviceId(QString deviceId); + void setHomeserver(QString homeserver); + void setDisableCertificateValidation(bool disabled); + void setHiddenTags(QStringList hiddenTags); + void setUseIdenticon(bool state); - QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } - bool messageHoverHighlight() const { return messageHoverHighlight_; } - bool enlargeEmojiOnlyMessages() const { return enlargeEmojiOnlyMessages_; } - bool tray() const { return tray_; } - bool startInTray() const { return startInTray_; } - bool groupView() const { return groupView_; } - bool avatarCircles() const { return avatarCircles_; } - bool decryptSidebar() const { return decryptSidebar_; } - bool privacyScreen() const { return privacyScreen_; } - int privacyScreenTimeout() const { return privacyScreenTimeout_; } - bool markdown() const { return markdown_; } - bool typingNotifications() const { return typingNotifications_; } - bool sortByImportance() const { return sortByImportance_; } - bool buttonsInTimeline() const { return buttonsInTimeline_; } - bool mobileMode() const { return mobileMode_; } - bool readReceipts() const { return readReceipts_; } - bool hasDesktopNotifications() const { return hasDesktopNotifications_; } - bool hasAlertOnNotification() const { return hasAlertOnNotification_; } - bool hasNotifications() const - { - return hasDesktopNotifications() || hasAlertOnNotification(); + QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } + bool messageHoverHighlight() const { return messageHoverHighlight_; } + bool enlargeEmojiOnlyMessages() const { return enlargeEmojiOnlyMessages_; } + bool tray() const { return tray_; } + bool startInTray() const { return startInTray_; } + bool groupView() const { return groupView_; } + bool avatarCircles() const { return avatarCircles_; } + bool decryptSidebar() const { return decryptSidebar_; } + bool privacyScreen() const { return privacyScreen_; } + int privacyScreenTimeout() const { return privacyScreenTimeout_; } + bool markdown() const { return markdown_; } + bool animateImagesOnHover() const { return animateImagesOnHover_; } + bool typingNotifications() const { return typingNotifications_; } + bool sortByImportance() const { return sortByImportance_; } + bool buttonsInTimeline() const { return buttonsInTimeline_; } + bool mobileMode() const { return mobileMode_; } + bool readReceipts() const { return readReceipts_; } + bool hasDesktopNotifications() const { return hasDesktopNotifications_; } + bool hasAlertOnNotification() const { return hasAlertOnNotification_; } + bool hasNotifications() const { return hasDesktopNotifications() || hasAlertOnNotification(); } + int timelineMaxWidth() const { return timelineMaxWidth_; } + int communityListWidth() const { return communityListWidth_; } + int roomListWidth() const { return roomListWidth_; } + double fontSize() const { return baseFontSize_; } + QString font() const { return font_; } + QString emojiFont() const + { + if (emojiFont_ == "Default") { + return tr("Default"); } - int timelineMaxWidth() const { return timelineMaxWidth_; } - int communityListWidth() const { return communityListWidth_; } - int roomListWidth() const { return roomListWidth_; } - double fontSize() const { return baseFontSize_; } - QString font() const { return font_; } - QString emojiFont() const - { - if (emojiFont_ == "Default") { - return tr("Default"); - } - return emojiFont_; - } - Presence presence() const { return presence_; } - QString ringtone() const { return ringtone_; } - QString microphone() const { return microphone_; } - QString camera() const { return camera_; } - QString cameraResolution() const { return cameraResolution_; } - QString cameraFrameRate() const { return cameraFrameRate_; } - int screenShareFrameRate() const { return screenShareFrameRate_; } - bool screenSharePiP() const { return screenSharePiP_; } - bool screenShareRemoteVideo() const { return screenShareRemoteVideo_; } - bool screenShareHideCursor() const { return screenShareHideCursor_; } - bool useStunServer() const { return useStunServer_; } - bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } - bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } - QString profile() const { return profile_; } - QString userId() const { return userId_; } - QString accessToken() const { return accessToken_; } - QString deviceId() const { return deviceId_; } - QString homeserver() const { return homeserver_; } - bool disableCertificateValidation() const { return disableCertificateValidation_; } - QStringList hiddenTags() const { return hiddenTags_; } + return emojiFont_; + } + Presence presence() const { return presence_; } + QString ringtone() const { return ringtone_; } + QString microphone() const { return microphone_; } + QString camera() const { return camera_; } + QString cameraResolution() const { return cameraResolution_; } + QString cameraFrameRate() const { return cameraFrameRate_; } + int screenShareFrameRate() const { return screenShareFrameRate_; } + bool screenSharePiP() const { return screenSharePiP_; } + bool screenShareRemoteVideo() const { return screenShareRemoteVideo_; } + bool screenShareHideCursor() const { return screenShareHideCursor_; } + bool useStunServer() const { return useStunServer_; } + bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } + bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } + bool useOnlineKeyBackup() const { return useOnlineKeyBackup_; } + QString profile() const { return profile_; } + QString userId() const { return userId_; } + QString accessToken() const { return accessToken_; } + QString deviceId() const { return deviceId_; } + QString homeserver() const { return homeserver_; } + bool disableCertificateValidation() const { return disableCertificateValidation_; } + QStringList hiddenTags() const { return hiddenTags_; } + bool useIdenticon() const { return useIdenticon_ && JdenticonProvider::isAvailable(); } signals: - void groupViewStateChanged(bool state); - void roomSortingChanged(bool state); - void themeChanged(QString state); - void messageHoverHighlightChanged(bool state); - void enlargeEmojiOnlyMessagesChanged(bool state); - void trayChanged(bool state); - void startInTrayChanged(bool state); - void markdownChanged(bool state); - void typingNotificationsChanged(bool state); - void buttonInTimelineChanged(bool state); - void readReceiptsChanged(bool state); - void desktopNotificationsChanged(bool state); - void alertOnNotificationChanged(bool state); - void avatarCirclesChanged(bool state); - void decryptSidebarChanged(bool state); - void privacyScreenChanged(bool state); - void privacyScreenTimeoutChanged(int state); - void timelineMaxWidthChanged(int state); - void roomListWidthChanged(int state); - void communityListWidthChanged(int state); - void mobileModeChanged(bool mode); - void fontSizeChanged(double state); - void fontChanged(QString state); - void emojiFontChanged(QString state); - void presenceChanged(Presence state); - void ringtoneChanged(QString ringtone); - void microphoneChanged(QString microphone); - void cameraChanged(QString camera); - void cameraResolutionChanged(QString resolution); - void cameraFrameRateChanged(QString frameRate); - void screenShareFrameRateChanged(int frameRate); - void screenSharePiPChanged(bool state); - void screenShareRemoteVideoChanged(bool state); - void screenShareHideCursorChanged(bool state); - void useStunServerChanged(bool state); - void onlyShareKeysWithVerifiedUsersChanged(bool state); - void shareKeysWithTrustedUsersChanged(bool state); - void profileChanged(QString profile); - void userIdChanged(QString userId); - void accessTokenChanged(QString accessToken); - void deviceIdChanged(QString deviceId); - void homeserverChanged(QString homeserver); - void disableCertificateValidationChanged(bool disabled); + void groupViewStateChanged(bool state); + void roomSortingChanged(bool state); + void themeChanged(QString state); + void messageHoverHighlightChanged(bool state); + void enlargeEmojiOnlyMessagesChanged(bool state); + void trayChanged(bool state); + void startInTrayChanged(bool state); + void markdownChanged(bool state); + void animateImagesOnHoverChanged(bool state); + void typingNotificationsChanged(bool state); + void buttonInTimelineChanged(bool state); + void readReceiptsChanged(bool state); + void desktopNotificationsChanged(bool state); + void alertOnNotificationChanged(bool state); + void avatarCirclesChanged(bool state); + void decryptSidebarChanged(bool state); + void privacyScreenChanged(bool state); + void privacyScreenTimeoutChanged(int state); + void timelineMaxWidthChanged(int state); + void roomListWidthChanged(int state); + void communityListWidthChanged(int state); + void mobileModeChanged(bool mode); + void fontSizeChanged(double state); + void fontChanged(QString state); + void emojiFontChanged(QString state); + void presenceChanged(Presence state); + void ringtoneChanged(QString ringtone); + void microphoneChanged(QString microphone); + void cameraChanged(QString camera); + void cameraResolutionChanged(QString resolution); + void cameraFrameRateChanged(QString frameRate); + void screenShareFrameRateChanged(int frameRate); + void screenSharePiPChanged(bool state); + void screenShareRemoteVideoChanged(bool state); + void screenShareHideCursorChanged(bool state); + void useStunServerChanged(bool state); + void onlyShareKeysWithVerifiedUsersChanged(bool state); + void shareKeysWithTrustedUsersChanged(bool state); + void useOnlineKeyBackupChanged(bool state); + void profileChanged(QString profile); + void userIdChanged(QString userId); + void accessTokenChanged(QString accessToken); + void deviceIdChanged(QString deviceId); + void homeserverChanged(QString homeserver); + void disableCertificateValidationChanged(bool disabled); + void useIdenticonChanged(bool state); private: - // Default to system theme if QT_QPA_PLATFORMTHEME var is set. - QString defaultTheme_ = - QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() - ? "light" - : "system"; - QString theme_; - bool messageHoverHighlight_; - bool enlargeEmojiOnlyMessages_; - bool tray_; - bool startInTray_; - bool groupView_; - bool markdown_; - bool typingNotifications_; - bool sortByImportance_; - bool buttonsInTimeline_; - bool readReceipts_; - bool hasDesktopNotifications_; - bool hasAlertOnNotification_; - bool avatarCircles_; - bool decryptSidebar_; - bool privacyScreen_; - int privacyScreenTimeout_; - bool shareKeysWithTrustedUsers_; - bool onlyShareKeysWithVerifiedUsers_; - bool mobileMode_; - int timelineMaxWidth_; - int roomListWidth_; - int communityListWidth_; - double baseFontSize_; - QString font_; - QString emojiFont_; - Presence presence_; - QString ringtone_; - QString microphone_; - QString camera_; - QString cameraResolution_; - QString cameraFrameRate_; - int screenShareFrameRate_; - bool screenSharePiP_; - bool screenShareRemoteVideo_; - bool screenShareHideCursor_; - bool useStunServer_; - bool disableCertificateValidation_ = false; - QString profile_; - QString userId_; - QString accessToken_; - QString deviceId_; - QString homeserver_; - QStringList hiddenTags_; + // Default to system theme if QT_QPA_PLATFORMTHEME var is set. + QString defaultTheme_ = + QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() + ? "light" + : "system"; + QString theme_; + bool messageHoverHighlight_; + bool enlargeEmojiOnlyMessages_; + bool tray_; + bool startInTray_; + bool groupView_; + bool markdown_; + bool animateImagesOnHover_; + bool typingNotifications_; + bool sortByImportance_; + bool buttonsInTimeline_; + bool readReceipts_; + bool hasDesktopNotifications_; + bool hasAlertOnNotification_; + bool avatarCircles_; + bool decryptSidebar_; + bool privacyScreen_; + int privacyScreenTimeout_; + bool shareKeysWithTrustedUsers_; + bool onlyShareKeysWithVerifiedUsers_; + bool useOnlineKeyBackup_; + bool mobileMode_; + int timelineMaxWidth_; + int roomListWidth_; + int communityListWidth_; + double baseFontSize_; + QString font_; + QString emojiFont_; + Presence presence_; + QString ringtone_; + QString microphone_; + QString camera_; + QString cameraResolution_; + QString cameraFrameRate_; + int screenShareFrameRate_; + bool screenSharePiP_; + bool screenShareRemoteVideo_; + bool screenShareHideCursor_; + bool useStunServer_; + bool disableCertificateValidation_ = false; + QString profile_; + QString userId_; + QString accessToken_; + QString deviceId_; + QString homeserver_; + QStringList hiddenTags_; + bool useIdenticon_; - QSettings settings; + QSettings settings; - static QSharedPointer instance_; + static QSharedPointer instance_; }; class HorizontalLine : public QFrame { - Q_OBJECT + Q_OBJECT public: - HorizontalLine(QWidget *parent = nullptr); + HorizontalLine(QWidget *parent = nullptr); }; class UserSettingsPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - UserSettingsPage(QSharedPointer settings, QWidget *parent = nullptr); + UserSettingsPage(QSharedPointer settings, QWidget *parent = nullptr); protected: - void showEvent(QShowEvent *event) override; - void paintEvent(QPaintEvent *event) override; + void showEvent(QShowEvent *event) override; + void paintEvent(QPaintEvent *event) override; signals: - void moveBack(); - void trayOptionChanged(bool value); - void themeChanged(); - void decryptSidebarChanged(); + void moveBack(); + void trayOptionChanged(bool value); + void themeChanged(); + void decryptSidebarChanged(); public slots: - void updateSecretStatus(); + void updateSecretStatus(); private slots: - void importSessionKeys(); - void exportSessionKeys(); + void importSessionKeys(); + void exportSessionKeys(); private: - // Layouts - QVBoxLayout *topLayout_; - QHBoxLayout *topBarLayout_; - QFormLayout *formLayout_; + // Layouts + QVBoxLayout *topLayout_; + QHBoxLayout *topBarLayout_; + QFormLayout *formLayout_; - // Shared settings object. - QSharedPointer settings_; + // Shared settings object. + QSharedPointer settings_; - Toggle *trayToggle_; - Toggle *startInTrayToggle_; - Toggle *groupViewToggle_; - Toggle *timelineButtonsToggle_; - Toggle *typingNotifications_; - Toggle *messageHoverHighlight_; - Toggle *enlargeEmojiOnlyMessages_; - Toggle *sortByImportance_; - Toggle *readReceipts_; - Toggle *markdown_; - Toggle *desktopNotifications_; - Toggle *alertOnNotification_; - Toggle *avatarCircles_; - Toggle *useStunServer_; - Toggle *decryptSidebar_; - Toggle *privacyScreen_; - QSpinBox *privacyScreenTimeout_; - Toggle *shareKeysWithTrustedUsers_; - Toggle *onlyShareKeysWithVerifiedUsers_; - Toggle *mobileMode_; - QLabel *deviceFingerprintValue_; - QLabel *deviceIdValue_; - QLabel *backupSecretCached; - QLabel *masterSecretCached; - QLabel *selfSigningSecretCached; - QLabel *userSigningSecretCached; + Toggle *trayToggle_; + Toggle *startInTrayToggle_; + Toggle *groupViewToggle_; + Toggle *timelineButtonsToggle_; + Toggle *typingNotifications_; + Toggle *messageHoverHighlight_; + Toggle *enlargeEmojiOnlyMessages_; + Toggle *sortByImportance_; + Toggle *readReceipts_; + Toggle *markdown_; + Toggle *animateImagesOnHover_; + Toggle *desktopNotifications_; + Toggle *alertOnNotification_; + Toggle *avatarCircles_; + Toggle *useIdenticon_; + Toggle *useStunServer_; + Toggle *decryptSidebar_; + Toggle *privacyScreen_; + QSpinBox *privacyScreenTimeout_; + Toggle *shareKeysWithTrustedUsers_; + Toggle *onlyShareKeysWithVerifiedUsers_; + Toggle *useOnlineKeyBackup_; + Toggle *mobileMode_; + QLabel *deviceFingerprintValue_; + QLabel *deviceIdValue_; + QLabel *backupSecretCached; + QLabel *masterSecretCached; + QLabel *selfSigningSecretCached; + QLabel *userSigningSecretCached; - QComboBox *themeCombo_; - QComboBox *scaleFactorCombo_; - QComboBox *fontSizeCombo_; - QFontComboBox *fontSelectionCombo_; - QComboBox *emojiFontSelectionCombo_; - QComboBox *ringtoneCombo_; - QComboBox *microphoneCombo_; - QComboBox *cameraCombo_; - QComboBox *cameraResolutionCombo_; - QComboBox *cameraFrameRateCombo_; + QComboBox *themeCombo_; + QComboBox *scaleFactorCombo_; + QComboBox *fontSizeCombo_; + QFontComboBox *fontSelectionCombo_; + QComboBox *emojiFontSelectionCombo_; + QComboBox *ringtoneCombo_; + QComboBox *microphoneCombo_; + QComboBox *cameraCombo_; + QComboBox *cameraResolutionCombo_; + QComboBox *cameraFrameRateCombo_; - QSpinBox *timelineMaxWidthSpin_; + QSpinBox *timelineMaxWidthSpin_; - int sideMargin_ = 0; + int sideMargin_ = 0; }; diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index c4379668..f82353cc 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -14,50 +14,51 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent) : QAbstractListModel(parent) , room_id(roomId) { - roomMembers_ = cache::roomMembers(roomId); - for (const auto &m : roomMembers_) { - displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); - userids.push_back(QString::fromStdString(m)); - } + roomMembers_ = cache::roomMembers(roomId); + for (const auto &m : roomMembers_) { + displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); + userids.push_back(QString::fromStdString(m)); + } } QHash UsersModel::roleNames() const { - return { - {CompletionModel::CompletionRole, "completionRole"}, - {CompletionModel::SearchRole, "searchRole"}, - {CompletionModel::SearchRole2, "searchRole2"}, - {Roles::DisplayName, "displayName"}, - {Roles::AvatarUrl, "avatarUrl"}, - {Roles::UserID, "userid"}, - }; + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::UserID, "userid"}, + }; } QVariant UsersModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - switch (role) { - case CompletionModel::CompletionRole: - if (UserSettings::instance()->markdown()) - return QString("[%1](https://matrix.to/#/%2)") - .arg(displayNames[index.row()]) - .arg(QString(QUrl::toPercentEncoding(userids[index.row()]))); - else - return displayNames[index.row()]; - case CompletionModel::SearchRole: - case Qt::DisplayRole: - case Roles::DisplayName: - return displayNames[index.row()]; - case CompletionModel::SearchRole2: - return userids[index.row()]; - case Roles::AvatarUrl: - return cache::avatarUrl(QString::fromStdString(room_id), - QString::fromStdString(roomMembers_[index.row()])); - case Roles::UserID: - return userids[index.row()]; - } + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: + if (UserSettings::instance()->markdown()) + return QString("[%1](https://matrix.to/#/%2)") + .arg(displayNames[index.row()].toHtmlEscaped()) + .arg(QString(QUrl::toPercentEncoding(userids[index.row()]))); + else + return displayNames[index.row()]; + case CompletionModel::SearchRole: + return displayNames[index.row()]; + case Qt::DisplayRole: + case Roles::DisplayName: + return displayNames[index.row()].toHtmlEscaped(); + case CompletionModel::SearchRole2: + return userids[index.row()]; + case Roles::AvatarUrl: + return cache::avatarUrl(QString::fromStdString(room_id), + QString::fromStdString(roomMembers_[index.row()])); + case Roles::UserID: + return userids[index.row()].toHtmlEscaped(); } - return {}; + } + return {}; } diff --git a/src/UsersModel.h b/src/UsersModel.h index 5bc94b0f..e719a8bd 100644 --- a/src/UsersModel.h +++ b/src/UsersModel.h @@ -9,25 +9,25 @@ class UsersModel : public QAbstractListModel { public: - enum Roles - { - AvatarUrl = Qt::UserRole, - DisplayName, - UserID, - }; + enum Roles + { + AvatarUrl = Qt::UserRole, + DisplayName, + UserID, + }; - UsersModel(const std::string &roomId, QObject *parent = nullptr); - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)roomMembers_.size(); - } - QVariant data(const QModelIndex &index, int role) const override; + UsersModel(const std::string &roomId, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomMembers_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; private: - std::string room_id; - std::vector roomMembers_; - std::vector displayNames; - std::vector userids; + std::string room_id; + std::vector roomMembers_; + std::vector displayNames; + std::vector userids; }; diff --git a/src/Utils.cpp b/src/Utils.cpp index 41013e39..b0fb01b1 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -38,154 +38,154 @@ template static DescInfo createDescriptionInfo(const Event &event, const QString &localUser, const QString &displayName) { - const auto msg = std::get(event); - const auto sender = QString::fromStdString(msg.sender); + const auto msg = std::get(event); + const auto sender = QString::fromStdString(msg.sender); - const auto username = displayName; - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - auto body = utils::event_body(event).trimmed(); - if (mtx::accessors::relations(event).reply_to()) - body = QString::fromStdString(utils::stripReplyFromBody(body.toStdString())); + const auto username = displayName; + const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); + auto body = utils::event_body(event).trimmed(); + if (mtx::accessors::relations(event).reply_to()) + body = QString::fromStdString(utils::stripReplyFromBody(body.toStdString())); - return DescInfo{QString::fromStdString(msg.event_id), - sender, - utils::messageDescription(username, body, sender == localUser), - utils::descriptiveTime(ts), - msg.origin_server_ts, - ts}; + return DescInfo{QString::fromStdString(msg.event_id), + sender, + utils::messageDescription(username, body, sender == localUser), + utils::descriptiveTime(ts), + msg.origin_server_ts, + ts}; } std::string utils::stripReplyFromBody(const std::string &bodyi) { - QString body = QString::fromStdString(bodyi); - QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); - while (body.startsWith(">")) - body.remove(plainQuote); - if (body.startsWith("\n")) - body.remove(0, 1); - return body.toStdString(); + QString body = QString::fromStdString(bodyi); + QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); + while (body.startsWith(">")) + body.remove(plainQuote); + if (body.startsWith("\n")) + body.remove(0, 1); + + body.replace("@room", QString::fromUtf8("@\u2060room")); + return body.toStdString(); } std::string utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi) { - QString formatted_body = QString::fromStdString(formatted_bodyi); - formatted_body.remove(QRegularExpression(".*", - QRegularExpression::DotMatchesEverythingOption)); - formatted_body.replace("@room", "@\u2060room"); - return formatted_body.toStdString(); + QString formatted_body = QString::fromStdString(formatted_bodyi); + formatted_body.remove(QRegularExpression(".*", + QRegularExpression::DotMatchesEverythingOption)); + formatted_body.replace("@room", QString::fromUtf8("@\u2060room")); + return formatted_body.toStdString(); } RelatedInfo utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_) { - RelatedInfo related = {}; - related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); - related.related_event = std::move(id); - related.type = mtx::accessors::msg_type(event); + RelatedInfo related = {}; + related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); + related.related_event = std::move(id); + related.type = mtx::accessors::msg_type(event); - // get body, strip reply fallback, then transform the event to text, if it is a media event - // etc - related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); - related.quoted_body = - QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString())); - related.quoted_body = utils::getQuoteBody(related); - related.quoted_body.replace("@room", QString::fromUtf8("@\u2060room")); + // get body, strip reply fallback, then transform the event to text, if it is a media event + // etc + related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); + related.quoted_body = + QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString())); + related.quoted_body = utils::getQuoteBody(related); - // get quoted body and strip reply fallback - related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); - related.quoted_formatted_body = QString::fromStdString( - stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString())); - related.room = room_id_; + // get quoted body and strip reply fallback + related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); + related.quoted_formatted_body = QString::fromStdString( + stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString())); + related.room = room_id_; - return related; + return related; } QString utils::localUser() { - return QString::fromStdString(http::client()->user_id().to_string()); + return QString::fromStdString(http::client()->user_id().to_string()); } bool utils::codepointIsEmoji(uint code) { - // TODO: Be more precise here. - return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) || - (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f; + // TODO: Be more precise here. + return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) || + (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f; } QString utils::replaceEmoji(const QString &body) { - QString fmtBody; - fmtBody.reserve(body.size()); + QString fmtBody; + fmtBody.reserve(body.size()); - QVector utf32_string = body.toUcs4(); + QVector utf32_string = body.toUcs4(); - bool insideFontBlock = false; - for (auto &code : utf32_string) { - if (utils::codepointIsEmoji(code)) { - if (!insideFontBlock) { - fmtBody += QStringLiteral("emojiFont() % - QStringLiteral("\">"); - insideFontBlock = true; - } + bool insideFontBlock = false; + for (auto &code : utf32_string) { + if (utils::codepointIsEmoji(code)) { + if (!insideFontBlock) { + fmtBody += QStringLiteral("emojiFont() % + QStringLiteral("\">"); + insideFontBlock = true; + } - } else { - if (insideFontBlock) { - fmtBody += QStringLiteral(""); - insideFontBlock = false; - } - } - if (QChar::requiresSurrogates(code)) { - QChar emoji[] = {static_cast(QChar::highSurrogate(code)), - static_cast(QChar::lowSurrogate(code))}; - fmtBody.append(emoji, 2); - } else { - fmtBody.append(QChar(static_cast(code))); - } - } - if (insideFontBlock) { + } else { + if (insideFontBlock) { fmtBody += QStringLiteral(""); + insideFontBlock = false; + } } + if (QChar::requiresSurrogates(code)) { + QChar emoji[] = {static_cast(QChar::highSurrogate(code)), + static_cast(QChar::lowSurrogate(code))}; + fmtBody.append(emoji, 2); + } else { + fmtBody.append(QChar(static_cast(code))); + } + } + if (insideFontBlock) { + fmtBody += QStringLiteral(""); + } - return fmtBody; + return fmtBody; } void utils::setScaleFactor(float factor) { - if (factor < 1 || factor > 3) - return; + if (factor < 1 || factor > 3) + return; - QSettings settings; - settings.setValue("settings/scale_factor", factor); + QSettings settings; + settings.setValue("settings/scale_factor", factor); } float utils::scaleFactor() { - QSettings settings; - return settings.value("settings/scale_factor", -1).toFloat(); + QSettings settings; + return settings.value("settings/scale_factor", -1).toFloat(); } QString utils::descriptiveTime(const QDateTime &then) { - const auto now = QDateTime::currentDateTime(); - const auto days = then.daysTo(now); + const auto now = QDateTime::currentDateTime(); + const auto days = then.daysTo(now); - if (days == 0) - return QLocale::system().toString(then.time(), QLocale::ShortFormat); - else if (days < 2) - return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); - else if (days < 7) - return then.toString("dddd"); + if (days == 0) + return QLocale::system().toString(then.time(), QLocale::ShortFormat); + else if (days < 2) + return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); + else if (days < 7) + return then.toString("dddd"); - return QLocale::system().toString(then.date(), QLocale::ShortFormat); + return QLocale::system().toString(then.date(), QLocale::ShortFormat); } DescInfo @@ -193,630 +193,622 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUser, const QString &displayName) { - using Audio = mtx::events::RoomEvent; - using Emote = mtx::events::RoomEvent; - using File = mtx::events::RoomEvent; - using Image = mtx::events::RoomEvent; - using Notice = mtx::events::RoomEvent; - using Text = mtx::events::RoomEvent; - using Video = mtx::events::RoomEvent; - using CallInvite = mtx::events::RoomEvent; - using CallAnswer = mtx::events::RoomEvent; - using CallHangUp = mtx::events::RoomEvent; - using Encrypted = mtx::events::EncryptedEvent; + using Audio = mtx::events::RoomEvent; + using Emote = mtx::events::RoomEvent; + using File = mtx::events::RoomEvent; + using Image = mtx::events::RoomEvent; + using Notice = mtx::events::RoomEvent; + using Text = mtx::events::RoomEvent; + using Video = mtx::events::RoomEvent; + using CallInvite = mtx::events::RoomEvent; + using CallAnswer = mtx::events::RoomEvent; + using CallHangUp = mtx::events::RoomEvent; + using Encrypted = mtx::events::EncryptedEvent; - if (std::holds_alternative